diff --git a/CHANGES.md b/CHANGES.md index 863b1ca455..9be8d85871 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,13 +5,26 @@ Features ✨: - Improvements 🙌: - - + - The initial sync is now handled by a foreground service + - Render aliases and canonical alias change in the timeline + - Fix autocompletion issues and add support for rooms and groups + - Introduce developer mode in the settings (#745, #796) + - Improve devices list screen + - Add settings for rageshake sensibility + - Fix autocompletion issues and add support for rooms, groups, and emoji (#780) + - Show skip to bottom FAB while scrolling down (#752) Other changes: - - + - Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800) + - Exclude play-services-oss-licenses library from F-Droid build (#814) Bugfix 🐛: - - + - Fix crash when opening room creation screen from the room filtering screen + - Fix avatar image disappearing (#777) + - Fix read marker banner when permalink + - Fix joining upgraded rooms (#697) + - Fix matrix.org room directory not being browsable (#807) + - Hide non working settings (#751) Translations 🗣: - 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/SingleThreadCoroutineDispatcher.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt index e63123f3b3..f2fbde3fe7 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt @@ -19,4 +19,4 @@ package im.vector.matrix.android import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.Dispatchers.Main -internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main) +internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticationServiceTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticationServiceTest.kt deleted file mode 100644 index c3babd7e5a..0000000000 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticationServiceTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.auth - -import androidx.test.annotation.UiThreadTest -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.GrantPermissionRule -import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.OkReplayRuleChainNoActivity -import im.vector.matrix.android.api.auth.AuthenticationService -import okreplay.* -import org.junit.ClassRule -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -internal class AuthenticationServiceTest : InstrumentedTest { - - lateinit var authenticationService: AuthenticationService - lateinit var okReplayInterceptor: OkReplayInterceptor - - private val okReplayConfig = OkReplayConfig.Builder() - .tapeRoot(AndroidTapeRoot( - context(), javaClass)) - .defaultMode(TapeMode.READ_WRITE) // or TapeMode.READ_ONLY - .sslEnabled(true) - .interceptor(okReplayInterceptor) - .build() - - @get:Rule - val testRule = OkReplayRuleChainNoActivity(okReplayConfig).get() - - @Test - @UiThreadTest - @OkReplay(tape = "auth", mode = TapeMode.READ_WRITE) - fun auth() { - } - - companion object { - @ClassRule - @JvmField - val grantExternalStoragePermissionRule: GrantPermissionRule = - GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - } -} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt index 3fc3079cc7..df503f2486 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt @@ -16,20 +16,31 @@ package im.vector.matrix.android.internal.crypto +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import io.realm.Realm import org.junit.Assert.* +import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.matrix.olm.OlmAccount import org.matrix.olm.OlmManager import org.matrix.olm.OlmSession private const val DUMMY_DEVICE_KEY = "DeviceKey" -class CryptoStoreTest { +@RunWith(AndroidJUnit4::class) +class CryptoStoreTest : InstrumentedTest { private val cryptoStoreHelper = CryptoStoreHelper() + @Before + fun setup() { + Realm.init(context()) + } + @Test fun test_metadata_realm_ok() { val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() 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 abb990c979..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,8 +19,12 @@ 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 import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent @@ -28,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 @@ -43,7 +46,11 @@ internal class ChunkEntityTest : InstrumentedTest { @Before fun setup() { Realm.init(context()) - val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build() + val testConfig = RealmConfiguration.Builder() + .inMemory() + .name("test-realm") + .modules(SessionRealmModule()) + .build() monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build() } @@ -141,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 -> @@ -172,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 } @@ -186,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/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index 06651f9ba3..008508ae19 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -66,7 +66,7 @@ internal class TimelineTest : InstrumentedTest { // val latch = CountDownLatch(2) // var timelineEvents: List = emptyList() // timeline.listener = object : Timeline.Listener { -// override fun onUpdated(snapshot: List) { +// override fun onTimelineUpdated(snapshot: List) { // if (snapshot.isNotEmpty()) { // if (initialLoad == 0) { // initialLoad = snapshot.size diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt index 685a522f60..bada3f86a1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt @@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint() ?.chunked(4) ?.joinToString(separator = " ") -fun MutableList.sortByLastSeen() { - sortWith(DatedObjectComparators.descComparator) +/* ========================================================================================== + * DeviceInfo + * ========================================================================================== */ + +fun List.sortByLastSeen(): List { + val list = toMutableList() + list.sortWith(DatedObjectComparators.descComparator) + return list } diff --git a/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt similarity index 60% rename from vector/src/main/java/im/vector/riotx/core/error/Extensions.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt index 614340bd3d..a3b5ce39eb 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt @@ -14,13 +14,15 @@ * limitations under the License. */ -package im.vector.riotx.core.error +package im.vector.matrix.android.api.failure -import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.failure.MatrixError import javax.net.ssl.HttpsURLConnection -fun Throwable.is401(): Boolean { - return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ - && error.code == MatrixError.M_UNAUTHORIZED) -} +fun Throwable.is401() = + this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && error.code == MatrixError.M_UNAUTHORIZED + +fun Throwable.isTokenError() = + this is Failure.ServerError + && (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt index fc02cf4a61..cd4ce1206e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.api.permalinks import android.text.Spannable -import im.vector.matrix.android.api.MatrixPatterns /** * MatrixLinkify take a piece of text and turns all of the @@ -30,7 +29,13 @@ object MatrixLinkify { * * @param spannable the text in which the matrix items has to be clickable. */ + @Suppress("UNUSED_PARAMETER") fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean { + /** + * I disable it because it mess up with pills, and even with pills, it does not work correctly: + * The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to + */ + /* // sanity checks if (spannable.isEmpty()) { return false @@ -50,5 +55,7 @@ object MatrixLinkify { } } return hasMatch + */ + return false } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt index d10152f4fe..871c30e46a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt @@ -56,23 +56,23 @@ object PermalinkParser { val identifier = params.getOrNull(0) val extraParameter = params.getOrNull(1) - if (identifier.isNullOrEmpty()) { - return PermalinkData.FallbackLink(uri) - } return when { + identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isRoomId(identifier) -> { - val eventId = extraParameter.takeIf { - !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) - } - PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId) + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = false, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) } MatrixPatterns.isRoomAlias(identifier) -> { - val eventId = extraParameter.takeIf { - !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) - } - PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId) + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = true, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) } else -> PermalinkData.FallbackLink(uri) } 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 1c1a3600c8..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,12 @@ 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 + */ + fun hasAlreadySynced(): Boolean /** * This method allow to close a session. It does stop some services. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt index a84e5af48c..2f34922280 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.api.MatrixCallback interface CacheService { /** - * Clear the whole cached data, except credentials. Once done, the session is closed and has to be opened again + * Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user. */ fun clearCache(callback: MatrixCallback) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 706f89dfc9..986cbb698b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody @@ -89,6 +90,8 @@ interface CryptoService { fun getDevicesList(callback: MatrixCallback) + fun getDeviceInfo(deviceId: String, callback: MatrixCallback) + fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int fun isRoomEncrypted(roomId: String): Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 38c24fa89b..22bf564a8a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -50,10 +50,10 @@ object EventType { const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" const val STATE_ROOM_ALIASES = "m.room.aliases" const val STATE_ROOM_TOMBSTONE = "m.room.tombstone" - const val STATE_CANONICAL_ALIAS = "m.room.canonical_alias" - const val STATE_HISTORY_VISIBILITY = "m.room.history_visibility" - const val STATE_RELATED_GROUPS = "m.room.related_groups" - const val STATE_PINNED_EVENT = "m.room.pinned_events" + const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias" + const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility" + const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups" + const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" // Call Events @@ -86,10 +86,12 @@ object EventType { STATE_ROOM_JOIN_RULES, STATE_ROOM_GUEST_ACCESS, STATE_ROOM_POWER_LEVELS, + STATE_ROOM_ALIASES, STATE_ROOM_TOMBSTONE, - STATE_HISTORY_VISIBILITY, - STATE_RELATED_GROUPS, - STATE_PINNED_EVENT + STATE_ROOM_CANONICAL_ALIAS, + STATE_ROOM_HISTORY_VISIBILITY, + STATE_ROOM_RELATED_GROUPS, + STATE_ROOM_PINNED_EVENT ) fun isStateEvent(type: String): Boolean { 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 ff63d1a9e7..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 @@ -31,9 +31,22 @@ interface GroupService { */ fun getGroup(groupId: String): Group? + /** + * Get a groupSummary from a groupId + * @param groupId the groupId to look for. + * @return the groupSummary with groupId or null + */ + 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 afe7cf8bc3..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 @@ -53,16 +53,35 @@ interface RoomService { fun getRoom(roomId: String): Room? /** - * Get a live list of room summaries. This list is refreshed as soon as the data changes. - * @return the [LiveData] of [RoomSummary] + * Get a roomSummary from a roomId or a room alias + * @param roomIdOrAlias the roomId or the alias of a room to look for. + * @return a matching room summary or null */ - fun liveRoomSummaries(): LiveData> + fun getRoomSummary(roomIdOrAlias: String): RoomSummary? + + /** + * Get a snapshot list of room summaries. + * @return the immutable list of [RoomSummary] + */ + 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/RoomCanonicalAliasContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt index a66f23555d..0aec7f6c8b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt @@ -20,7 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass /** - * Class representing the EventType.STATE_CANONICAL_ALIAS state event content + * Class representing the EventType.STATE_ROOM_CANONICAL_ALIAS state event content */ @JsonClass(generateAdapter = true) data class RoomCanonicalAliasContent( 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 8dd924f0f0..582a10a4f3 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 @@ -43,7 +43,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/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index 598aab2d30..bc1e941698 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -145,13 +145,13 @@ class CreateRoomParams { */ fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) { // Remove the existing value if any. - initialStates?.removeAll { it.getClearType() == EventType.STATE_HISTORY_VISIBILITY } + initialStates?.removeAll { it.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY } if (historyVisibility != null) { val contentMap = HashMap() contentMap["history_visibility"] = historyVisibility - val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY, + val historyVisibilityEvent = Event(type = EventType.STATE_ROOM_HISTORY_VISIBILITY, stateKey = "", content = contentMap.toContent()) 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 b3dd1c6f22..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 @@ -98,7 +98,7 @@ interface RelationService { /** * Reply to an event in the timeline (must be in same room) * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 - * The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated + * The replyText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated * by the sdk into pills. * @param eventReplied the event referenced by the reply * @param replyText the reply text @@ -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/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/MatrixItemSpan.kt similarity index 90% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/MatrixItemSpan.kt index 71a422bac8..d191f5197b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/MatrixItemSpan.kt @@ -19,9 +19,9 @@ package im.vector.matrix.android.api.session.room.send import im.vector.matrix.android.api.util.MatrixItem /** - * Tag class for spans that should mention a user. + * Tag class for spans that should mention a matrix item. * These Spans will be transformed into pills when detected in message to send */ -interface UserMentionSpan { +interface MatrixItemSpan { val matrixItem: MatrixItem } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index bdae5eaaa6..ac1b50bbcb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -29,7 +29,7 @@ interface SendService { /** * Method to send a text message asynchronously. - * The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated + * The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated * by the sdk into pills. * @param text the text message to send * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 85dbdcaa19..2280803e5c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -65,7 +65,7 @@ interface Timeline { /** * This is the main method to enrich the timeline with new data. - * It will call the onUpdated method from [Listener] when the data will be processed. + * It will call the onTimelineUpdated method from [Listener] when the data will be processed. * It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row. */ fun paginate(direction: Direction, count: Int) @@ -106,7 +106,12 @@ interface Timeline { * Call when the timeline has been updated through pagination or sync. * @param snapshot the most up to date snapshot */ - fun onUpdated(snapshot: List) + fun onTimelineUpdated(snapshot: List) + + /** + * Called whenever an error we can't recover from occurred + */ + fun onTimelineFailure(throwable: Throwable) } /** 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 4fed773ae2..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 @@ -62,6 +63,9 @@ sealed class MatrixItem( init { if (BuildConfig.DEBUG) checkId() } + + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + override fun getBestName() = id } data class GroupItem(override val id: String, @@ -71,9 +75,12 @@ sealed class MatrixItem( init { if (BuildConfig.DEBUG) checkId() } + + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + override fun getBestName() = id } - fun getBestName(): String { + open fun getBestName(): String { return displayName?.takeIf { it.isNotBlank() } ?: id } @@ -95,7 +102,7 @@ sealed class MatrixItem( } fun firstLetterOfDisplayName(): String { - return getBestName() + return (displayName?.takeIf { it.isNotBlank() } ?: id) .let { dn -> var startIndex = 0 val initial = dn[startIndex] @@ -138,4 +145,6 @@ sealed class MatrixItem( fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl) 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/auth/SessionId.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt new file mode 100644 index 0000000000..2f39806aa5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt @@ -0,0 +1,23 @@ +/* + * 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.auth + +import im.vector.matrix.android.internal.util.md5 + +internal fun createSessionId(userId: String, deviceId: String?): String { + return (if (deviceId.isNullOrBlank()) userId else "$userId|$deviceId").md5() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt index 83bf7b7822..7d7f8cc22c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt @@ -16,6 +16,9 @@ package im.vector.matrix.android.internal.auth.db +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.auth.createSessionId +import im.vector.matrix.android.internal.di.MoshiProvider import io.realm.DynamicRealm import io.realm.RealmMigration import timber.log.Timber @@ -23,35 +26,60 @@ import timber.log.Timber internal object AuthRealmMigration : RealmMigration { // Current schema version - const val SCHEMA_VERSION = 2L + const val SCHEMA_VERSION = 3L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") - if (oldVersion <= 0) { - Timber.d("Step 0 -> 1") - Timber.d("Create PendingSessionEntity") + if (oldVersion <= 0) migrateTo1(realm) + if (oldVersion <= 1) migrateTo2(realm) + if (oldVersion <= 2) migrateTo3(realm) + } - realm.schema.create("PendingSessionEntity") - .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) - .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) - .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java) - .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true) - .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java) - .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true) - .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java) - .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java) - .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) - .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) - } + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Create PendingSessionEntity") - if (oldVersion <= 1) { - Timber.d("Step 1 -> 2") - Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") + realm.schema.create("PendingSessionEntity") + .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) + .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) + .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java) + .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true) + .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java) + .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true) + .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java) + .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java) + .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) + .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) + } - realm.schema.get("SessionParamsEntity") - ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) - ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } - } + private fun migrateTo2(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") + + realm.schema.get("SessionParamsEntity") + ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) + ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } + } + + private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") + Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId") + + realm.schema.get("SessionParamsEntity") + ?.removePrimaryKey() + ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java) + ?.setRequired(SessionParamsEntityFields.SESSION_ID, true) + ?.transform { + val userId = it.getString(SessionParamsEntityFields.USER_ID) + val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON) + + val credentials = MoshiProvider.providesMoshi() + .adapter(Credentials::class.java) + .fromJson(credentialsJson) + + it.set(SessionParamsEntityFields.SESSION_ID, createSessionId(userId, credentials?.deviceId)) + } + ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt index 92511dccf7..72eed95fcc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt @@ -20,7 +20,8 @@ import io.realm.RealmObject import io.realm.annotations.PrimaryKey internal open class SessionParamsEntity( - @PrimaryKey var userId: String = "", + @PrimaryKey var sessionId: String = "", + var userId: String = "", var credentialsJson: String = "", var homeServerConnectionConfigJson: String = "", // Set to false when the token is invalid and the user has been soft logged out diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt index 72e8087f3f..d4ba1eb818 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.Moshi import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.internal.auth.createSessionId import javax.inject.Inject internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { @@ -49,6 +50,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { return null } return SessionParamsEntity( + createSessionId(sessionParams.credentials.userId, sessionParams.credentials.deviceId), sessionParams.credentials.userId, credentialsJson, homeServerConnectionConfigJson, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index a12f6e40ce..9bf3d227c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -47,7 +47,7 @@ internal abstract class CryptoModule { @Module companion object { - internal const val DB_ALIAS_PREFIX = "crypto_module_" + internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5" @JvmStatic @Provides @@ -59,7 +59,7 @@ internal abstract class CryptoModule { return RealmConfiguration.Builder() .directory(directory) .apply { - realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5") + realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5)) } .name("crypto_store.realm") .modules(RealmCryptoStoreModule()) @@ -123,6 +123,9 @@ internal abstract class CryptoModule { @Binds abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask + @Binds + abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask + @Binds abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index c50b9e2e10..be8918dac7 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody @@ -127,6 +128,7 @@ internal class DefaultCryptoService @Inject constructor( private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask, // Tasks private val getDevicesTask: GetDevicesTask, + private val getDeviceInfoTask: GetDeviceInfoTask, private val setDeviceNameTask: SetDeviceNameTask, private val uploadKeysTask: UploadKeysTask, private val loadRoomMembersTask: LoadRoomMembersTask, @@ -145,17 +147,17 @@ internal class DefaultCryptoService @Inject constructor( fun onStateEvent(roomId: String, event: Event) { when { - event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } } fun onLiveEvent(roomId: String, event: Event) { when { - event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } } @@ -199,6 +201,14 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } + override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) { + getDeviceInfoTask + .configureWith(GetDeviceInfoTask.Params(deviceId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt index f4821f8ef3..b2e880c2f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt @@ -25,11 +25,18 @@ internal interface CryptoApi { /** * Get the devices list - * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices + * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices") fun getDevices(): Call + /** + * Get the device info by id + * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}") + fun getDeviceInfo(@Path("deviceId") deviceId: String): Call + /** * Upload device and/or one-time keys. * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt new file mode 100644 index 0000000000..f97e86a57d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt @@ -0,0 +1,37 @@ +/* + * 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.crypto.tasks + +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface GetDeviceInfoTask : Task { + data class Params(val deviceId: String) +} + +internal class DefaultGetDeviceInfoTask @Inject constructor(private val cryptoApi: CryptoApi) + : GetDeviceInfoTask { + + override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo { + return executeRequest { + apiCall = cryptoApi.getDeviceInfo(params.deviceId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index 9c7a788b44..ee6d8e507c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -19,12 +19,16 @@ package im.vector.matrix.android.internal.database import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.util.createBackgroundHandler import io.realm.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference internal interface LiveEntityObserver { fun start() fun dispose() + fun cancelProcess() fun isStarted(): Boolean } @@ -35,6 +39,7 @@ internal abstract class RealmLiveEntityObserver(protected val r val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND") } + protected val observerScope = CoroutineScope(SupervisorJob()) protected abstract val query: Monarchy.Query private val isStarted = AtomicBoolean(false) private val backgroundRealm = AtomicReference() @@ -59,10 +64,15 @@ internal abstract class RealmLiveEntityObserver(protected val r backgroundRealm.getAndSet(null).also { it.close() } + observerScope.coroutineContext.cancelChildren() } } } + override fun cancelProcess() { + observerScope.coroutineContext.cancelChildren() + } + override fun isStarted(): Boolean { return isStarted.get() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt index 2d386eac15..98544d46f7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt @@ -16,43 +16,36 @@ package im.vector.matrix.android.internal.database -import im.vector.matrix.android.internal.util.createBackgroundHandler import io.realm.* -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.* -class RealmQueryLatch(private val realmConfiguration: RealmConfiguration, - private val realmQueryBuilder: (Realm) -> RealmQuery) { +internal suspend fun awaitNotEmptyResult(realmConfiguration: RealmConfiguration, + timeoutMillis: Long, + builder: (Realm) -> RealmQuery) { + withTimeout(timeoutMillis) { + // Confine Realm interaction to a single thread with Looper. + withContext(Dispatchers.Main) { + val latch = CompletableDeferred() - private companion object { - val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH") - } + Realm.getInstance(realmConfiguration).use { realm -> + val result = builder(realm).findAllAsync() - @Throws(InterruptedException::class) - fun await(timeout: Long, timeUnit: TimeUnit) { - val realmRef = AtomicReference() - val latch = CountDownLatch(1) - QUERY_LATCH_HANDLER.post { - val realm = Realm.getInstance(realmConfiguration) - realmRef.set(realm) - val result = realmQueryBuilder(realm).findAllAsync() - result.addChangeListener(object : RealmChangeListener> { - override fun onChange(t: RealmResults) { - if (t.isNotEmpty()) { - result.removeChangeListener(this) - latch.countDown() + val listener = object : RealmChangeListener> { + override fun onChange(it: RealmResults) { + if (it.isNotEmpty()) { + result.removeChangeListener(this) + latch.complete(Unit) + } } } - }) - } - try { - latch.await(timeout, timeUnit) - } catch (exception: InterruptedException) { - throw exception - } finally { - QUERY_LATCH_HANDLER.post { - realmRef.getAndSet(null).close() + + result.addChangeListener(listener) + try { + latch.await() + } catch (e: CancellationException) { + result.removeChangeListener(listener) + throw e + } } } } 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 bc806a56a4..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 @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database import android.content.Context import im.vector.matrix.android.internal.database.model.SessionRealmModule +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.UserCacheDirectory import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.session.SessionModule @@ -37,13 +38,14 @@ private const val REALM_NAME = "disk_store.realm" */ internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils, @UserCacheDirectory val directory: File, + @SessionId val sessionId: String, @UserMd5 val userMd5: String, context: Context) { private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) fun create(): RealmConfiguration { - val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) + val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) if (shouldClearRealm) { Timber.v("************************************************************") Timber.v("The realm file session was corrupted and couldn't be loaded.") @@ -53,14 +55,15 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val } sharedPreferences .edit() - .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", true) + .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true) .apply() val realmConfiguration = RealmConfiguration.Builder() + .compactOnLaunch() .directory(directory) .name(REALM_NAME) .apply { - realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5") + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) } .modules(SessionRealmModule()) .deleteRealmIfMigrationNeeded() @@ -71,7 +74,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val Timber.v("Successfully create realm instance") sharedPreferences .edit() - .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) + .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) .apply() } return realmConfiguration 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 826b35254e..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 @@ -21,27 +21,14 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity -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.find import im.vector.matrix.android.internal.database.query.getOrCreate 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() @@ -51,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 } @@ -70,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) { @@ -134,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 @@ -156,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() @@ -170,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 b5ab54b744..483247c576 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 @@ -72,7 +72,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/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index c17864b82b..04b8565546 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.olm.OlmManager -import java.util.concurrent.Executors @Module internal object MatrixModule { @@ -38,8 +37,7 @@ internal object MatrixModule { return MatrixCoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default, main = Dispatchers.Main, - crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), - sync = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher() ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt index 0e38618590..32649443db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -31,3 +31,10 @@ internal annotation class UserId @Qualifier @Retention(AnnotationRetention.RUNTIME) internal annotation class UserMd5 + +/** + * Used to inject the sessionId, which is defined as md5(userId|deviceId) + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index 3d850c223a..6565b8685b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.network import android.content.Context +import androidx.annotation.WorkerThread import com.novoda.merlin.Merlin import com.novoda.merlin.MerlinsBeard import im.vector.matrix.android.internal.di.MatrixScope @@ -28,8 +29,8 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @MatrixScope -internal class NetworkConnectivityChecker @Inject constructor(context: Context, - backgroundDetectionObserver: BackgroundDetectionObserver) +internal class NetworkConnectivityChecker @Inject constructor(private val context: Context, + private val backgroundDetectionObserver: BackgroundDetectionObserver) : BackgroundDetectionObserver.Listener { private val merlin = Merlin.Builder() @@ -37,19 +38,33 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context, .withDisconnectableCallbacks() .build(context) - private val listeners = Collections.synchronizedSet(LinkedHashSet()) + private val merlinsBeard = MerlinsBeard.Builder().build(context) - // True when internet is available - var hasInternetAccess = MerlinsBeard.Builder().build(context).isConnected - private set + private val listeners = Collections.synchronizedSet(LinkedHashSet()) + private var hasInternetAccess = merlinsBeard.isConnected init { backgroundDetectionObserver.register(this) } + /** + * Returns true when internet is available + */ + @WorkerThread + fun hasInternetAccess(): Boolean { + // If we are in background we have unbound merlin, so we have to check + return if (backgroundDetectionObserver.isInBackground) { + merlinsBeard.hasInternetAccess() + } else { + hasInternetAccess + } + } + override fun onMoveToForeground() { merlin.bind() - + merlinsBeard.hasInternetAccess { + hasInternetAccess = it + } merlin.registerDisconnectable { if (hasInternetAccess) { Timber.v("On Disconnect") @@ -76,14 +91,17 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context, merlin.unbind() } + // In background you won't get notification as merlin is unbound suspend fun waitUntilConnected() { if (hasInternetAccess) { return } else { + Timber.v("Waiting for network...") suspendCoroutine { continuation -> register(object : Listener { override fun onConnect() { unregister(this) + Timber.v("Connected to network...") continuation.resume(Unit) } }) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt new file mode 100644 index 0000000000..2bc05eacec --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt @@ -0,0 +1,33 @@ +/* + * 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 io.realm.RealmObject +import io.realm.RealmQuery + +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/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index c160ac9b31..66b94cf68d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments -import im.vector.matrix.android.internal.di.UserMd5 +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.md5 @@ -42,7 +42,7 @@ import java.io.IOException import javax.inject.Inject internal class DefaultFileService @Inject constructor(private val context: Context, - @UserMd5 private val userMd5: String, + @SessionId private val sessionId: String, private val contentUrlResolver: ContentUrlResolver, private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { @@ -103,9 +103,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte return when (downloadMode) { FileService.DownloadMode.FOR_INTERNAL_USE -> { // Create dir tree (MF stands for Matrix File): - // /MF/// + // /MF/// val tmpFolderRoot = File(context.cacheDir, "MF") - val tmpFolderUser = File(tmpFolderRoot, userMd5) + val tmpFolderUser = File(tmpFolderRoot, sessionId) File(tmpFolderUser, id.md5()) } FileService.DownloadMode.TO_EXPORT -> { 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 d6a0206eca..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 @@ -45,6 +45,8 @@ import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.database.LiveEntityObserver +import im.vector.matrix.android.internal.session.sync.SyncTaskSequencer +import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncWorker import kotlinx.coroutines.Dispatchers @@ -76,24 +78,26 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val secureStorageService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, + private val syncTokenStore: SyncTokenStore, + private val syncTaskSequencer: SyncTaskSequencer, private val sessionParamsStore: SessionParamsStore, private val contentUploadProgressTracker: ContentUploadStateTracker, private val initialSyncProgressService: Lazy, private val homeServerCapabilitiesService: Lazy) : Session, - RoomService by roomService.get(), - RoomDirectoryService by roomDirectoryService.get(), - GroupService by groupService.get(), - UserService by userService.get(), - CryptoService by cryptoService.get(), - SignOutService by signOutService.get(), - FilterService by filterService.get(), - PushRuleService by pushRuleService.get(), - PushersService by pushersService.get(), - FileService by fileService.get(), - InitialSyncProgressService by initialSyncProgressService.get(), - SecureStorageService by secureStorageService.get(), - HomeServerCapabilitiesService by homeServerCapabilitiesService.get() { + RoomService by roomService.get(), + RoomDirectoryService by roomDirectoryService.get(), + GroupService by groupService.get(), + UserService by userService.get(), + CryptoService by cryptoService.get(), + SignOutService by signOutService.get(), + FilterService by filterService.get(), + PushRuleService by pushRuleService.get(), + PushersService by pushersService.get(), + FileService by fileService.get(), + InitialSyncProgressService by initialSyncProgressService.get(), + SecureStorageService by secureStorageService.get(), + HomeServerCapabilitiesService by homeServerCapabilitiesService.get() { private var isOpen = false @@ -149,12 +153,17 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se cryptoService.get().close() isOpen = false EventBus.getDefault().unregister(this) + syncTaskSequencer.close() } - override fun syncState(): LiveData { + override fun getSyncStateLive(): LiveData { return getSyncThread().liveState() } + override fun hasAlreadySynced(): Boolean { + return syncTokenStore.getLastToken() != null + } + private fun getSyncThread(): SyncThread { return syncThread ?: syncThreadProvider.get().also { syncThread = it @@ -164,23 +173,14 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se override fun clearCache(callback: MatrixCallback) { stopSync() stopAnyBackgroundSync() - cacheService.get().clearCache(object : MatrixCallback { - override fun onSuccess(data: Unit) { - startSync(true) - callback.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - startSync(true) - callback.onFailure(failure) - } - }) + liveEntityObservers.forEach { it.cancelProcess() } + cacheService.get().clearCache(callback) } @Subscribe(threadMode = ThreadMode.MAIN) fun onGlobalError(globalError: GlobalError) { if (globalError is GlobalError.InvalidToken - && globalError.softLogout) { + && globalError.softLogout) { // Mark the token has invalid GlobalScope.launch(Dispatchers.IO) { sessionParamsStore.setTokenInvalid(myUserId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 8c16050442..a208a7a720 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -46,6 +46,7 @@ import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.user.UserModule import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @Component(dependencies = [MatrixComponent::class], modules = [ @@ -69,6 +70,8 @@ import im.vector.matrix.android.internal.task.TaskExecutor @SessionScope internal interface SessionComponent { + fun coroutineDispatchers(): MatrixCoroutineDispatchers + fun session(): Session fun syncTask(): SyncTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 0e88894969..437a559ea1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService +import im.vector.matrix.android.internal.auth.createSessionId import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.* @@ -54,8 +55,7 @@ internal abstract class SessionModule { @Module companion object { - - internal const val DB_ALIAS_PREFIX = "session_db_" + internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5" @JvmStatic @Provides @@ -83,11 +83,26 @@ internal abstract class SessionModule { return userId.md5() } + @JvmStatic + @SessionId + @Provides + fun providesSessionId(credentials: Credentials): String { + return createSessionId(credentials.userId, credentials.deviceId) + } + @JvmStatic @Provides @UserCacheDirectory - fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File { - return File(context.filesDir, userMd5) + fun providesFilesDir(@UserMd5 userMd5: String, + @SessionId sessionId: String, + context: Context): File { + // Temporary code for migration + val old = File(context.filesDir, userMd5) + if (old.exists()) { + old.renameTo(File(context.filesDir, sessionId)) + } + + return File(context.filesDir, sessionId) } @JvmStatic 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 be059038f3..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,11 +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 { @@ -33,10 +38,30 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon return null } - override fun liveGroupSummaries(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) }, + override fun getGroupSummary(groupId: String): GroupSummary? { + return monarchy.fetchCopyMap( + { realm -> GroupSummaryEntity.where(realm, groupId).findFirst() }, + { it, _ -> it.asDomain() } + ) + } + + 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/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index 9eceb44417..553a0387c5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -22,6 +22,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.query.where @@ -31,6 +32,7 @@ import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWor import im.vector.matrix.android.internal.worker.WorkerParamsFactory import io.realm.OrderedCollectionChangeSet import io.realm.RealmResults +import kotlinx.coroutines.launch import javax.inject.Inject private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" @@ -49,14 +51,19 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont .mapNotNull { results[it] } fetchGroupsData(modifiedGroupEntity - .filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE } - .map { it.groupId } - .toList()) + .filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE } + .map { it.groupId } + .toList()) - deleteGroups(modifiedGroupEntity + modifiedGroupEntity .filter { it.membership == Membership.LEAVE } .map { it.groupId } - .toList()) + .toList() + .also { + observerScope.launch { + deleteGroups(it) + } + } } private fun fetchGroupsData(groupIds: List) { @@ -77,12 +84,9 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont /** * Delete the GroupSummaryEntity of left groups */ - private fun deleteGroups(groupIds: List) { - monarchy - .writeAsync { realm -> - GroupSummaryEntity.where(realm, groupIds) - .findAll() - .deleteAllFromRealm() - } + private suspend fun deleteGroups(groupIds: List) = awaitTransaction(monarchy.realmConfiguration) { realm -> + GroupSummaryEntity.where(realm, groupIds) + .findAll() + .deleteAllFromRealm() } } 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 de60e6e7e4..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 @@ -30,7 +31,9 @@ import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.model.RoomEntity 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 @@ -38,7 +41,9 @@ import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask 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, @@ -69,30 +74,66 @@ 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 getRoomSummary(roomIdOrAlias: String): RoomSummary? { + return monarchy + .fetchCopyMap({ + if (roomIdOrAlias.startsWith("!")) { + // It's a roomId + RoomSummaryEntity.where(it, roomId = roomIdOrAlias).findFirst() + } else { + // Assume it's a room alias + RoomSummaryEntity.findByAlias(it, roomIdOrAlias) + } + }, { entity, _ -> + roomSummaryMapper.map(entity) + }) + } + + 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/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index aadf1bfccf..4a14005fe9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -23,11 +23,10 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.UserId -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration import io.realm.RealmResults +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -39,8 +38,7 @@ import javax.inject.Inject internal class EventRelationsAggregationUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, @UserId private val userId: String, - private val task: EventRelationsAggregationTask, - private val taskExecutor: TaskExecutor) : + private val task: EventRelationsAggregationTask) : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { @@ -63,6 +61,8 @@ internal class EventRelationsAggregationUpdater @Inject constructor(@SessionData insertedDomains, userId ) - task.configureWith(params).executeBy(taskExecutor) + observerScope.launch { + task.execute(params) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index c5b3f03d35..6896788de9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -212,11 +212,12 @@ internal interface RoomAPI { /** * Join the given room. * - * @param roomId the room id + * @param roomIdOrAlias the room id or alias + * @param server_name the servers to attempt to join the room through * @param params the request body */ - @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") - fun join(@Path("roomId") roomId: String, + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}") + fun join(@Path("roomIdOrAlias") roomIdOrAlias: String, @Query("server_name") viaServers: List, @Body params: Map): Call 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/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 30a2948f68..a21a3b4a8d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper +import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService @@ -35,6 +36,7 @@ internal interface RoomFactory { fun create(roomId: String): Room } +@SessionScope internal class DefaultRoomFactory @Inject constructor(private val monarchy: Monarchy, private val roomSummaryMapper: RoomSummaryMapper, private val cryptoService: CryptoService, 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 126d13c5db..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,10 +24,11 @@ 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.* import im.vector.matrix.android.internal.database.query.isEventRead import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.prev @@ -38,7 +39,6 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications import io.realm.Realm -import io.realm.kotlin.createObject import javax.inject.Inject internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId: String, @@ -52,7 +52,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, - EventType.STATE_HISTORY_VISIBILITY, + EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, @@ -69,9 +69,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId roomSummary: RoomSyncSummary? = null, unreadNotifications: RoomSyncUnreadNotifications? = null, updateMembers: Boolean = false) { - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) - + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) if (roomSummary != null) { if (roomSummary.heroes.isNotEmpty()) { roomSummaryEntity.heroes.clear() @@ -93,12 +91,13 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() - val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).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) @@ -107,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/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt index 9af8434b7c..970a1fed7e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt @@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse -import im.vector.matrix.android.internal.database.RealmQueryLatch +import im.vector.matrix.android.internal.database.awaitNotEmptyResult import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -34,6 +34,7 @@ import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAcco import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -53,13 +54,12 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro } val roomId = createRoomResponse.roomId!! // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) - val rql = RealmQueryLatch(realmConfiguration) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) - } try { - rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) - } catch (exception: Exception) { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + } catch (exception: TimeoutCancellationException) { throw CreateRoomFailure.CreatedWithTimeout } if (params.isDirect()) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt index 010023596c..1553ddec04 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -30,9 +31,9 @@ import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import io.realm.OrderedCollectionChangeSet -import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmResults +import kotlinx.coroutines.launch import javax.inject.Inject internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase @@ -51,21 +52,21 @@ internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase } .toList() .also { - handleRoomCreateEvents(it) + observerScope.launch { + handleRoomCreateEvents(it) + } } } - private fun handleRoomCreateEvents(createEvents: List) = Realm.getInstance(realmConfiguration).use { - it.executeTransactionAsync { realm -> - for (event in createEvents) { - val createRoomContent = event.getClearContent().toModel() - val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue + private suspend fun handleRoomCreateEvents(createEvents: List) = awaitTransaction(realmConfiguration) { realm -> + for (event in createEvents) { + val createRoomContent = event.getClearContent().toModel() + val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue - val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() - ?: RoomSummaryEntity(predecessorRoomId) - predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED - realm.insertOrUpdate(predecessorRoomSummary) - } + val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() + ?: RoomSummaryEntity(predecessorRoomId) + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED + realm.insertOrUpdate(predecessorRoomSummary) } } } 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 2271631932..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 @@ -62,7 +63,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return@doWithRealm } - val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev() + val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev() name = ContentMapper.map(canonicalAlias?.content).toModel()?.canonicalAlias if (!name.isNullOrEmpty()) { return@doWithRealm @@ -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/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt index 7304c09d57..fbede72520 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.session.room.membership.joining import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure -import im.vector.matrix.android.internal.database.RealmQueryLatch +import im.vector.matrix.android.internal.database.awaitNotEmptyResult import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.di.SessionDatabase @@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.task.Task import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -46,18 +47,16 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room executeRequest { apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason)) } - val roomId = params.roomId // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) - val rql = RealmQueryLatch(realmConfiguration) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) - } try { - rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) - } catch (exception: Exception) { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, params.roomId) + } + } catch (exception: TimeoutCancellationException) { throw JoinRoomFailure.JoinedWithTimeout } - setReadMarkers(roomId) + setReadMarkers(params.roomId) } private suspend fun setReadMarkers(roomId: String) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index d66a2f6e57..b29d3210bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -23,11 +23,10 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration import io.realm.RealmResults +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -36,8 +35,7 @@ import javax.inject.Inject * As it will actually delete the content, it should be called last in the list of listener. */ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, - private val pruneEventTask: PruneEventTask, - private val taskExecutor: TaskExecutor) : + private val pruneEventTask: PruneEventTask) : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { EventEntity.types(it, listOf(EventType.REDACTION)) } @@ -50,7 +48,9 @@ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfigurat .mapNotNull { results[it]?.asDomain() } .toList() - val params = PruneEventTask.Params(insertedDomains) - pruneEventTask.configureWith(params).executeBy(taskExecutor) + observerScope.launch { + val params = PruneEventTask.Params(insertedDomains) + pruneEventTask.execute(params) + } } } 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 c303e1c215..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,21 +97,20 @@ 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) } } private fun computeAllowedKeys(type: String): List { // Add filtered content, allowed keys in content depends on the event type return when (type) { - EventType.STATE_ROOM_MEMBER -> listOf("membership") - EventType.STATE_ROOM_CREATE -> listOf("creator") - EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") - EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", + EventType.STATE_ROOM_MEMBER -> listOf("membership") + EventType.STATE_ROOM_CREATE -> listOf("creator") + EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") + EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", "users_default", "events", "events_default", @@ -117,10 +119,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M "kick", "redact", "invite") - EventType.STATE_ROOM_ALIASES -> listOf("aliases") - EventType.STATE_CANONICAL_ALIAS -> listOf("alias") - EventType.FEEDBACK -> listOf("type", "target_event_id") - else -> emptyList() + EventType.STATE_ROOM_ALIASES -> listOf("aliases") + EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias") + EventType.FEEDBACK -> listOf("type", "target_event_id") + else -> emptyList() } } } 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/send/pills/MentionLinkSpec.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt index 5ad61b5441..055eade5e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt @@ -16,10 +16,10 @@ package im.vector.matrix.android.internal.session.room.send.pills -import im.vector.matrix.android.api.session.room.send.UserMentionSpan +import im.vector.matrix.android.api.session.room.send.MatrixItemSpan internal data class MentionLinkSpec( - val span: UserMentionSpan, + val span: MatrixItemSpan, val start: Int, val end: Int ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index c079e456c0..1a7b8228b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt @@ -16,15 +16,13 @@ package im.vector.matrix.android.internal.session.room.send.pills import android.text.SpannableString -import im.vector.matrix.android.api.session.room.send.UserMentionSpan +import im.vector.matrix.android.api.session.room.send.MatrixItemSpan import java.util.* import javax.inject.Inject /** * Utility class to detect special span in CharSequence and turn them into * formatted text to send them as a Matrix messages. - * - * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) */ internal class TextPillsUtils @Inject constructor( private val mentionLinkSpecComparator: MentionLinkSpecComparator @@ -49,7 +47,7 @@ internal class TextPillsUtils @Inject constructor( private fun transformPills(text: CharSequence, template: String): String? { val spannableString = SpannableString.valueOf(text) val pills = spannableString - ?.getSpans(0, text.length, UserMentionSpan::class.java) + ?.getSpans(0, text.length, MatrixItemSpan::class.java) ?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) } ?.toMutableList() ?.takeIf { it.isNotEmpty() } @@ -65,7 +63,7 @@ internal class TextPillsUtils @Inject constructor( // append text before pill append(text, currIndex, start) // append the pill - append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName)) + append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.getBestName())) currIndex = end } // append text after the last pill 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 693855edbc..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() @@ -504,15 +489,14 @@ internal class DefaultTimeline( Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask .configureWith(params) { - this.retryCount = Int.MAX_VALUE this.constraints = TaskConstraints(connectedToNetwork = true) 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 -> @@ -524,6 +508,8 @@ internal class DefaultTimeline( } override fun onFailure(failure: Throwable) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + postSnapshot() Timber.v("Failure fetching $limit items $direction from pagination request") } } @@ -637,7 +623,14 @@ internal class DefaultTimeline( private fun fetchEvent(eventId: String) { val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize) - cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor) + cancelableBag += contextOfEventTask.configureWith(params) { + callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + postFailure(failure) + } + } + } + .executeBy(taskExecutor) } private fun postSnapshot() { @@ -648,16 +641,26 @@ internal class DefaultTimeline( updateLoadingStates(filteredEvents) val snapshot = createSnapshot() val runnable = Runnable { - synchronized(listeners) { - listeners.forEach { - it.onUpdated(snapshot) - } + listeners.forEach { + it.onTimelineUpdated(snapshot) } } debouncer.debounce("post_snapshot", runnable, 50) } } + private fun postFailure(throwable: Throwable) { + if (isReady.get().not()) { + return + } + val runnable = Runnable { + listeners.forEach { + it.onTimelineFailure(throwable) + } + } + mainHandler.post(runnable) + } + private fun clearAllValues() { prevDisplayIndex = null nextDisplayIndex = null 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 0d9fb4e9e6..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 @@ -24,7 +24,6 @@ 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 @@ -33,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) { /** *
@@ -112,7 +112,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
                     Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
 
                     val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                                     ?: realm.createObject(roomId)
+                            ?: realm.createObject(roomId)
 
                     val nextToken: String?
                     val prevToken: String?
@@ -125,34 +125,29 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
                     }
 
                     val shouldSkip = ChunkEntity.find(realm, roomId, nextToken = nextToken) != null
-                                     || ChunkEntity.find(realm, roomId, prevToken = prevToken) != null
+                            || ChunkEntity.find(realm, roomId, prevToken = prevToken) != null
 
                     val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
                     val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
 
                     // 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) {
@@ -170,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()) {
@@ -196,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/room/tombstone/RoomTombstoneEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt
index 301c383a6d..e5e538ae89 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt
@@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
 import im.vector.matrix.android.api.session.room.model.VersioningState
 import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
 import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
+import im.vector.matrix.android.internal.database.awaitTransaction
 import im.vector.matrix.android.internal.database.mapper.asDomain
 import im.vector.matrix.android.internal.database.model.EventEntity
 import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
@@ -30,9 +31,9 @@ import im.vector.matrix.android.internal.database.query.types
 import im.vector.matrix.android.internal.database.query.where
 import im.vector.matrix.android.internal.di.SessionDatabase
 import io.realm.OrderedCollectionChangeSet
-import io.realm.Realm
 import io.realm.RealmConfiguration
 import io.realm.RealmResults
+import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase
@@ -51,24 +52,24 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba
                 }
                 .toList()
                 .also {
-                    handleRoomTombstoneEvents(it)
+                    observerScope.launch {
+                        handleRoomTombstoneEvents(it)
+                    }
                 }
     }
 
-    private fun handleRoomTombstoneEvents(tombstoneEvents: List) = Realm.getInstance(realmConfiguration).use {
-        it.executeTransactionAsync { realm ->
-            for (event in tombstoneEvents) {
-                if (event.roomId == null) continue
-                val createRoomContent = event.getClearContent().toModel()
-                if (createRoomContent?.replacementRoom == null) continue
+    private suspend fun handleRoomTombstoneEvents(tombstoneEvents: List) = awaitTransaction(realmConfiguration) { realm ->
+        for (event in tombstoneEvents) {
+            if (event.roomId == null) continue
+            val createRoomContent = event.getClearContent().toModel()
+            if (createRoomContent?.replacementRoom == null) continue
 
-                val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst()
-                                             ?: RoomSummaryEntity(event.roomId)
-                if (predecessorRoomSummary.versioningState == VersioningState.NONE) {
-                    predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED
-                }
-                realm.insertOrUpdate(predecessorRoomSummary)
+            val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst()
+                                         ?: RoomSummaryEntity(event.roomId)
+            if (predecessorRoomSummary.versioningState == VersioningState.NONE) {
+                predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED
             }
+            realm.insertOrUpdate(predecessorRoomSummary)
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
index 51cb22c988..b43bfa603c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
@@ -97,8 +97,8 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
         userFile.deleteRecursively()
 
         Timber.d("SignOut: clear the database keys")
-        realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5)
-        realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5)
+        realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
+        realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
 
         // Sanity check
         if (BuildConfig.DEBUG) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt
index 2ca9b6cccc..392db0a73c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt
@@ -16,7 +16,6 @@
 
 package im.vector.matrix.android.internal.session.sync
 
-import com.zhuinden.monarchy.Monarchy
 import im.vector.matrix.android.R
 import im.vector.matrix.android.api.session.room.model.Membership
 import im.vector.matrix.android.internal.database.model.GroupEntity
@@ -25,11 +24,10 @@ import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressServi
 import im.vector.matrix.android.internal.session.mapWithProgress
 import im.vector.matrix.android.internal.session.sync.model.GroupsSyncResponse
 import im.vector.matrix.android.internal.session.sync.model.InvitedGroupSync
-import im.vector.matrix.android.internal.util.awaitTransaction
 import io.realm.Realm
 import javax.inject.Inject
 
-internal class GroupSyncHandler @Inject constructor(private val monarchy: Monarchy) {
+internal class GroupSyncHandler @Inject constructor() {
 
     sealed class HandlingStrategy {
         data class JOINED(val data: Map) : HandlingStrategy()
@@ -37,12 +35,14 @@ internal class GroupSyncHandler @Inject constructor(private val monarchy: Monarc
         data class LEFT(val data: Map) : HandlingStrategy()
     }
 
-    suspend fun handle(roomsSyncResponse: GroupsSyncResponse, reporter: DefaultInitialSyncProgressService? = null) {
-        monarchy.awaitTransaction { realm ->
-            handleGroupSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter)
-            handleGroupSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter)
-            handleGroupSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter)
-        }
+    fun handle(
+            realm: Realm,
+            roomsSyncResponse: GroupsSyncResponse,
+            reporter: DefaultInitialSyncProgressService? = null
+    ) {
+        handleGroupSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter)
+        handleGroupSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter)
+        handleGroupSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter)
     }
 
     // PRIVATE METHODS *****************************************************************************
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 4a003eb7d9..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
@@ -16,9 +16,7 @@
 
 package im.vector.matrix.android.internal.session.sync
 
-import com.zhuinden.monarchy.Monarchy
 import im.vector.matrix.android.R
-import im.vector.matrix.android.api.pushrules.RuleScope
 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
@@ -29,36 +27,29 @@ 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.notification.DefaultPushRuleService
-import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
 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 im.vector.matrix.android.internal.task.TaskExecutor
-import im.vector.matrix.android.internal.task.configureWith
-import im.vector.matrix.android.internal.util.awaitTransaction
 import io.realm.Realm
 import io.realm.kotlin.createObject
 import timber.log.Timber
 import javax.inject.Inject
 
-internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarchy,
-                                                   private val readReceiptHandler: ReadReceiptHandler,
+internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler,
                                                    private val roomSummaryUpdater: RoomSummaryUpdater,
                                                    private val roomTagHandler: RoomTagHandler,
                                                    private val roomFullyReadHandler: RoomFullyReadHandler,
                                                    private val cryptoService: DefaultCryptoService,
-                                                   private val tokenStore: SyncTokenStore,
-                                                   private val pushRuleService: DefaultPushRuleService,
-                                                   private val processForPushTask: ProcessEventForPushTask,
-                                                   private val taskExecutor: TaskExecutor) {
+                                                   private val roomMemberEventHandler: RoomMemberEventHandler,
+                                                   private val timelineEventSenderVisitor: TimelineEventSenderVisitor) {
 
     sealed class HandlingStrategy {
         data class JOINED(val data: Map) : HandlingStrategy()
@@ -66,28 +57,16 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
         data class LEFT(val data: Map) : HandlingStrategy()
     }
 
-    suspend fun handle(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService? = null) {
+    fun handle(
+            realm: Realm,
+            roomsSyncResponse: RoomsSyncResponse,
+            isInitialSync: Boolean,
+            reporter: DefaultInitialSyncProgressService? = null
+    ) {
         Timber.v("Execute transaction from $this")
-        monarchy.awaitTransaction { realm ->
-            handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter)
-            handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter)
-            handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter)
-        }
-        // handle event for bing rule checks
-        checkPushRules(roomsSyncResponse)
-    }
-
-    private fun checkPushRules(roomsSyncResponse: RoomsSyncResponse) {
-        Timber.v("[PushRules] --> checkPushRules")
-        if (tokenStore.getLastToken() == null) {
-            Timber.v("[PushRules] <-- No push rule check on initial sync")
-            return
-        } // nothing on initial sync
-
-        val rules = pushRuleService.getPushRules(RuleScope.GLOBAL)
-        processForPushTask.configureWith(ProcessEventForPushTask.Params(roomsSyncResponse, rules))
-                .executeBy(taskExecutor)
-        Timber.v("[PushRules] <-- Push task scheduled")
+        handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter)
+        handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter)
+        handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter)
     }
 
     // PRIVATE METHODS *****************************************************************************
@@ -137,15 +116,13 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
 
         if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
             val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
-                                ?: Int.MIN_VALUE
+                    ?: Int.MIN_VALUE
             val untimelinedStateIndex = minStateIndex + 1
             roomSync.state.events.forEach { event ->
                 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()) {
@@ -213,11 +190,13 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
         }
         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
@@ -230,11 +209,9 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
                     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/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt
index 1ae185b073..1454fdae7d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt
@@ -16,20 +16,30 @@
 
 package im.vector.matrix.android.internal.session.sync
 
+import com.zhuinden.monarchy.Monarchy
 import im.vector.matrix.android.R
+import im.vector.matrix.android.api.pushrules.PushRuleService
+import im.vector.matrix.android.api.pushrules.RuleScope
 import im.vector.matrix.android.internal.crypto.DefaultCryptoService
 import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
+import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
 import im.vector.matrix.android.internal.session.reportSubtask
+import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
 import im.vector.matrix.android.internal.session.sync.model.SyncResponse
+import im.vector.matrix.android.internal.util.awaitTransaction
 import timber.log.Timber
 import javax.inject.Inject
 import kotlin.system.measureTimeMillis
 
-internal class SyncResponseHandler @Inject constructor(private val roomSyncHandler: RoomSyncHandler,
+internal class SyncResponseHandler @Inject constructor(private val monarchy: Monarchy,
+                                                       private val roomSyncHandler: RoomSyncHandler,
                                                        private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
                                                        private val groupSyncHandler: GroupSyncHandler,
                                                        private val cryptoSyncHandler: CryptoSyncHandler,
                                                        private val cryptoService: DefaultCryptoService,
+                                                       private val tokenStore: SyncTokenStore,
+                                                       private val processEventForPushTask: ProcessEventForPushTask,
+                                                       private val pushRuleService: PushRuleService,
                                                        private val initialSyncProgressService: DefaultInitialSyncProgressService) {
 
     suspend fun handleResponse(syncResponse: SyncResponse, fromToken: String?) {
@@ -45,26 +55,27 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
         }.also {
             Timber.v("Finish handling start cryptoService in $it ms")
         }
-        val measure = measureTimeMillis {
-            // Handle the to device events before the room ones
-            // to ensure to decrypt them properly
-            measureTimeMillis {
-                Timber.v("Handle toDevice")
-                reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) {
-                    if (syncResponse.toDevice != null) {
-                        cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter)
-                    }
-                }
-            }.also {
-                Timber.v("Finish handling toDevice in $it ms")
-            }
 
+        // Handle the to device events before the room ones
+        // to ensure to decrypt them properly
+        measureTimeMillis {
+            Timber.v("Handle toDevice")
+            reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) {
+                if (syncResponse.toDevice != null) {
+                    cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter)
+                }
+            }
+        }.also {
+            Timber.v("Finish handling toDevice in $it ms")
+        }
+
+        // Start one big transaction
+        monarchy.awaitTransaction { realm ->
             measureTimeMillis {
                 Timber.v("Handle rooms")
-
                 reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) {
                     if (syncResponse.rooms != null) {
-                        roomSyncHandler.handle(syncResponse.rooms, isInitialSync, reporter)
+                        roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, reporter)
                     }
                 }
             }.also {
@@ -75,7 +86,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
                 reportSubtask(reporter, R.string.initial_sync_start_importing_account_groups, 100, 0.1f) {
                     Timber.v("Handle groups")
                     if (syncResponse.groups != null) {
-                        groupSyncHandler.handle(syncResponse.groups, reporter)
+                        groupSyncHandler.handle(realm, syncResponse.groups, reporter)
                     }
                 }
             }.also {
@@ -85,15 +96,32 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
             measureTimeMillis {
                 reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) {
                     Timber.v("Handle accountData")
-                    userAccountDataSyncHandler.handle(syncResponse.accountData, syncResponse.rooms?.invite)
+                    userAccountDataSyncHandler.handle(realm, syncResponse.accountData)
                 }
             }.also {
                 Timber.v("Finish handling accountData in $it ms")
             }
-
-            Timber.v("On sync completed")
-            cryptoSyncHandler.onSyncCompleted(syncResponse)
+            tokenStore.saveToken(realm, syncResponse.nextBatch)
         }
-        Timber.v("Finish handling sync in $measure ms")
+
+        // Everything else we need to do outside the transaction
+        syncResponse.rooms?.also {
+            checkPushRules(it, isInitialSync)
+            userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
+        }
+        Timber.v("On sync completed")
+        cryptoSyncHandler.onSyncCompleted(syncResponse)
+    }
+
+    private suspend fun checkPushRules(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean) {
+        Timber.v("[PushRules] --> checkPushRules")
+        if (isInitialSync) {
+            Timber.v("[PushRules] <-- No push rule check on initial sync")
+            return
+        } // nothing on initial sync
+
+        val rules = pushRuleService.getPushRules(RuleScope.GLOBAL)
+        processEventForPushTask.execute(ProcessEventForPushTask.Params(roomsSyncResponse, rules))
+        Timber.v("[PushRules] <-- Push task scheduled")
     }
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
index 9f9e67bd2e..d99b9df4df 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
@@ -17,7 +17,6 @@
 package im.vector.matrix.android.internal.session.sync
 
 import im.vector.matrix.android.R
-import im.vector.matrix.android.internal.auth.SessionParamsStore
 import im.vector.matrix.android.internal.di.UserId
 import im.vector.matrix.android.internal.network.executeRequest
 import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
@@ -26,6 +25,7 @@ import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabil
 import im.vector.matrix.android.internal.session.sync.model.SyncResponse
 import im.vector.matrix.android.internal.session.user.UserStore
 import im.vector.matrix.android.internal.task.Task
+import timber.log.Timber
 import javax.inject.Inject
 
 internal interface SyncTask : Task {
@@ -37,14 +37,19 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
                                                    @UserId private val userId: String,
                                                    private val filterRepository: FilterRepository,
                                                    private val syncResponseHandler: SyncResponseHandler,
-                                                   private val sessionParamsStore: SessionParamsStore,
                                                    private val initialSyncProgressService: DefaultInitialSyncProgressService,
                                                    private val syncTokenStore: SyncTokenStore,
                                                    private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
-                                                   private val userStore: UserStore
+                                                   private val userStore: UserStore,
+                                                   private val syncTaskSequencer: SyncTaskSequencer
 ) : SyncTask {
 
-    override suspend fun execute(params: SyncTask.Params) {
+    override suspend fun execute(params: SyncTask.Params) = syncTaskSequencer.post {
+        doSync(params)
+    }
+
+    private suspend fun doSync(params: SyncTask.Params) {
+        Timber.v("Sync task started on Thread: ${Thread.currentThread().name}")
         // Maybe refresh the home server capabilities data we know
         getHomeServerCapabilitiesTask.execute(Unit)
 
@@ -69,9 +74,9 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
             apiCall = syncAPI.sync(requestParams)
         }
         syncResponseHandler.handleResponse(syncResponse, token)
-        syncTokenStore.saveToken(syncResponse.nextBatch)
         if (isInitialSync) {
             initialSyncProgressService.endAll()
         }
+        Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}")
     }
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt
new file mode 100644
index 0000000000..bfa49b7af5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.matrix.android.internal.session.sync
+
+import im.vector.matrix.android.internal.session.SessionScope
+import im.vector.matrix.android.internal.task.ChannelCoroutineSequencer
+import javax.inject.Inject
+
+@SessionScope
+internal class SyncTaskSequencer @Inject constructor() : ChannelCoroutineSequencer()
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt
index 350f2a1d83..d0af7d3ff5 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt
@@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.sync
 
 import com.zhuinden.monarchy.Monarchy
 import im.vector.matrix.android.internal.database.model.SyncEntity
-import im.vector.matrix.android.internal.util.awaitTransaction
 import io.realm.Realm
 import javax.inject.Inject
 
@@ -30,10 +29,8 @@ internal class SyncTokenStore @Inject constructor(private val monarchy: Monarchy
         }
     }
 
-    suspend fun saveToken(token: String?) {
-        monarchy.awaitTransaction {
-            val sync = SyncEntity(token)
-            it.insertOrUpdate(sync)
-        }
+    fun saveToken(realm: Realm, token: String?) {
+        val sync = SyncEntity(token)
+        realm.insertOrUpdate(sync)
     }
 }
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 9cc3a5a3c6..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
@@ -17,44 +17,39 @@
 package im.vector.matrix.android.internal.session.sync
 
 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.RoomSummaryEntity
+import im.vector.matrix.android.internal.database.model.*
 import im.vector.matrix.android.internal.database.query.getDirectRooms
+import im.vector.matrix.android.internal.database.query.getOrCreate
 import im.vector.matrix.android.internal.database.query.where
 import im.vector.matrix.android.internal.di.UserId
-import im.vector.matrix.android.internal.session.pushers.SavePushRulesTask
 import im.vector.matrix.android.internal.session.room.membership.RoomMembers
 import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
 import im.vector.matrix.android.internal.session.sync.model.accountdata.*
 import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
-import im.vector.matrix.android.internal.session.user.accountdata.SaveBreadcrumbsTask
-import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask
 import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
-import im.vector.matrix.android.internal.task.TaskExecutor
-import im.vector.matrix.android.internal.task.configureWith
-import im.vector.matrix.android.internal.util.awaitTransaction
 import io.realm.Realm
+import io.realm.RealmList
 import timber.log.Timber
 import javax.inject.Inject
 
 internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy,
                                                               @UserId private val userId: String,
                                                               private val directChatsHelper: DirectChatsHelper,
-                                                              private val updateUserAccountDataTask: UpdateUserAccountDataTask,
-                                                              private val savePushRulesTask: SavePushRulesTask,
-                                                              private val saveIgnoredUsersTask: SaveIgnoredUsersTask,
-                                                              private val saveBreadcrumbsTask: SaveBreadcrumbsTask,
-                                                              private val taskExecutor: TaskExecutor) {
+                                                              private val updateUserAccountDataTask: UpdateUserAccountDataTask) {
 
-    suspend fun handle(accountData: UserAccountDataSync?, invites: Map?) {
+    fun handle(realm: Realm, accountData: UserAccountDataSync?) {
         accountData?.list?.forEach {
             when (it) {
-                is UserAccountDataDirectMessages -> handleDirectChatRooms(it)
-                is UserAccountDataPushRules      -> handlePushRules(it)
-                is UserAccountDataIgnoredUsers   -> handleIgnoredUsers(it)
-                is UserAccountDataBreadcrumbs    -> handleBreadcrumbs(it)
+                is UserAccountDataDirectMessages -> handleDirectChatRooms(realm, it)
+                is UserAccountDataPushRules      -> handlePushRules(realm, it)
+                is UserAccountDataIgnoredUsers   -> handleIgnoredUsers(realm, it)
+                is UserAccountDataBreadcrumbs    -> handleBreadcrumbs(realm, it)
                 is UserAccountDataFallback       -> Timber.d("Receive account data of unhandled type ${it.type}")
                 else                             -> error("Missing code here!")
             }
@@ -65,78 +60,133 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
         //     it.toString()
         //     MoshiProvider.providesMoshi()
         // }
-
-        monarchy.doWithRealm { realm ->
-            synchronizeWithServerIfNeeded(realm, invites)
-        }
-    }
-
-    private suspend fun handlePushRules(userAccountDataPushRules: UserAccountDataPushRules) {
-        savePushRulesTask.execute(SavePushRulesTask.Params(userAccountDataPushRules.content))
-    }
-
-    private suspend fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) {
-        monarchy.awaitTransaction { realm ->
-            val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
-            oldDirectRooms.forEach {
-                it.isDirect = false
-                it.directUserId = null
-            }
-            directMessages.content.forEach {
-                val userId = it.key
-                it.value.forEach { roomId ->
-                    val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
-                    if (roomSummaryEntity != null) {
-                        roomSummaryEntity.isDirect = true
-                        roomSummaryEntity.directUserId = userId
-                        realm.insertOrUpdate(roomSummaryEntity)
-                    }
-                }
-            }
-        }
     }
 
     // If we get some direct chat invites, we synchronize the user account data including those.
-    private fun synchronizeWithServerIfNeeded(realm: Realm, invites: Map?) {
+    suspend fun synchronizeWithServerIfNeeded(invites: Map) {
         if (invites.isNullOrEmpty()) return
         val directChats = directChatsHelper.getLocalUserAccount()
         var hasUpdate = false
-        invites.forEach { (roomId, _) ->
-            val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId)
-            val inviterId = myUserStateEvent?.sender
-            val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
-            val isDirect = myUserRoomMember?.isDirect
-            if (inviterId != null && inviterId != userId && isDirect == true) {
-                directChats
-                        .getOrPut(inviterId, { arrayListOf() })
-                        .apply {
-                            if (contains(roomId)) {
-                                Timber.v("Direct chats already include room $roomId with user $inviterId")
-                            } else {
-                                add(roomId)
-                                hasUpdate = true
+        monarchy.doWithRealm { realm ->
+            invites.forEach { (roomId, _) ->
+                val myUserStateEvent = RoomMembers(realm, roomId).getLastStateEvent(userId)
+                val inviterId = myUserStateEvent?.sender
+                val myUserRoomMember: RoomMemberContent? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
+                val isDirect = myUserRoomMember?.isDirect
+                if (inviterId != null && inviterId != userId && isDirect == true) {
+                    directChats
+                            .getOrPut(inviterId, { arrayListOf() })
+                            .apply {
+                                if (contains(roomId)) {
+                                    Timber.v("Direct chats already include room $roomId with user $inviterId")
+                                } else {
+                                    add(roomId)
+                                    hasUpdate = true
+                                }
                             }
-                        }
+                }
             }
         }
         if (hasUpdate) {
             val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams(
                     directMessages = directChats
             )
-            updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor)
+            updateUserAccountDataTask.execute(updateUserAccountParams)
         }
     }
 
-    private fun handleIgnoredUsers(userAccountDataIgnoredUsers: UserAccountDataIgnoredUsers) {
-        saveIgnoredUsersTask
-                .configureWith(SaveIgnoredUsersTask.Params(userAccountDataIgnoredUsers.content.ignoredUsers.keys.toList()))
-                .executeBy(taskExecutor)
+    private fun handlePushRules(realm: Realm, userAccountDataPushRules: UserAccountDataPushRules) {
+        val pushRules = userAccountDataPushRules.content
+        realm.where(PushRulesEntity::class.java)
+                .findAll()
+                .deleteAllFromRealm()
+
+        // Save only global rules for the moment
+        val globalRules = pushRules.global
+
+        val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT }
+        globalRules.content?.forEach { rule ->
+            content.pushRules.add(PushRulesMapper.map(rule))
+        }
+        realm.insertOrUpdate(content)
+
+        val override = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.OVERRIDE }
+        globalRules.override?.forEach { rule ->
+            PushRulesMapper.map(rule).also {
+                override.pushRules.add(it)
+            }
+        }
+        realm.insertOrUpdate(override)
+
+        val rooms = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.ROOM }
+        globalRules.room?.forEach { rule ->
+            rooms.pushRules.add(PushRulesMapper.map(rule))
+        }
+        realm.insertOrUpdate(rooms)
+
+        val senders = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.SENDER }
+        globalRules.sender?.forEach { rule ->
+            senders.pushRules.add(PushRulesMapper.map(rule))
+        }
+        realm.insertOrUpdate(senders)
+
+        val underrides = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.UNDERRIDE }
+        globalRules.underride?.forEach { rule ->
+            underrides.pushRules.add(PushRulesMapper.map(rule))
+        }
+        realm.insertOrUpdate(underrides)
+    }
+
+    private fun handleDirectChatRooms(realm: Realm, directMessages: UserAccountDataDirectMessages) {
+        val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
+        oldDirectRooms.forEach {
+            it.isDirect = false
+            it.directUserId = null
+        }
+        directMessages.content.forEach {
+            val userId = it.key
+            it.value.forEach { roomId ->
+                val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
+                if (roomSummaryEntity != null) {
+                    roomSummaryEntity.isDirect = true
+                    roomSummaryEntity.directUserId = userId
+                    realm.insertOrUpdate(roomSummaryEntity)
+                }
+            }
+        }
+    }
+
+    private fun handleIgnoredUsers(realm: Realm, userAccountDataIgnoredUsers: UserAccountDataIgnoredUsers) {
+        val userIds = userAccountDataIgnoredUsers.content.ignoredUsers.keys
+        realm.where(IgnoredUserEntity::class.java)
+                .findAll()
+                .deleteAllFromRealm()
+        // And save the new received list
+        userIds.forEach { realm.createObject(IgnoredUserEntity::class.java).apply { userId = it } }
         // TODO If not initial sync, we should execute a init sync
     }
 
-    private fun handleBreadcrumbs(userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) {
-        saveBreadcrumbsTask
-                .configureWith(SaveBreadcrumbsTask.Params(userAccountDataBreadcrumbs.content.recentRoomIds))
-                .executeBy(taskExecutor)
+    private fun handleBreadcrumbs(realm: Realm, userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) {
+        val recentRoomIds = userAccountDataBreadcrumbs.content.recentRoomIds
+        val entity = BreadcrumbsEntity.getOrCreate(realm)
+
+        // And save the new received list
+        entity.recentRoomIds = RealmList().apply { addAll(recentRoomIds) }
+
+        // Update the room summaries
+        // Reset all the indexes...
+        RoomSummaryEntity.where(realm)
+                .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
+                .findAll()
+                .forEach {
+                    it.breadcrumbsIndex = RoomSummaryEntity.NOT_IN_BREADCRUMBS
+                }
+
+        // ...and apply new indexes
+        recentRoomIds.forEachIndexed { index, roomId ->
+            RoomSummaryEntity.where(realm, roomId)
+                    .findFirst()
+                    ?.breadcrumbsIndex = index
+        }
     }
 }
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 71734cdfe7..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
@@ -18,21 +18,18 @@ package im.vector.matrix.android.internal.session.sync.job
 import android.app.Service
 import android.content.Intent
 import android.os.IBinder
-import com.squareup.moshi.JsonEncodingException
 import im.vector.matrix.android.api.Matrix
-import im.vector.matrix.android.api.MatrixCallback
-import im.vector.matrix.android.api.failure.Failure
-import im.vector.matrix.android.api.failure.MatrixError
-import im.vector.matrix.android.api.util.Cancelable
+import im.vector.matrix.android.api.failure.isTokenError
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.sync.SyncState
 import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
 import im.vector.matrix.android.internal.session.sync.SyncTask
 import im.vector.matrix.android.internal.task.TaskExecutor
-import im.vector.matrix.android.internal.task.TaskThread
-import im.vector.matrix.android.internal.task.configureWith
+import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
+import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
+import kotlinx.coroutines.*
 import timber.log.Timber
-import java.net.SocketTimeoutException
-import java.util.Timer
-import java.util.TimerTask
+import java.util.concurrent.atomic.AtomicBoolean
 
 /**
  * Can execute periodic sync task.
@@ -40,33 +37,46 @@ import java.util.TimerTask
  * in order to be able to perform a sync even if the app is not running.
  * The  and  must be declared in the Manifest or the app using the SDK
  */
-open class SyncService : Service() {
+abstract class SyncService : Service() {
 
+    private var userId: String? = null
     private var mIsSelfDestroyed: Boolean = false
-    private var cancelableTask: Cancelable? = null
 
+    private var isInitialSync: Boolean = false
+    private lateinit var session: Session
     private lateinit var syncTask: SyncTask
     private lateinit var networkConnectivityChecker: NetworkConnectivityChecker
     private lateinit var taskExecutor: TaskExecutor
+    private lateinit var coroutineDispatchers: MatrixCoroutineDispatchers
+    private lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
 
-    var timer = Timer()
+    private val isRunning = AtomicBoolean(false)
+
+    private val serviceScope = CoroutineScope(SupervisorJob())
 
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
         Timber.i("onStartCommand $intent")
         intent?.let {
-            val userId = it.getStringExtra(EXTRA_USER_ID)
-            val sessionComponent = Matrix.getInstance(applicationContext).sessionManager.getSessionComponent(userId)
+            val matrix = Matrix.getInstance(applicationContext)
+            val safeUserId = it.getStringExtra(EXTRA_USER_ID) ?: return@let
+            val sessionComponent = matrix.sessionManager.getSessionComponent(safeUserId)
                     ?: return@let
+            session = sessionComponent.session()
+            userId = safeUserId
             syncTask = sessionComponent.syncTask()
+            isInitialSync = !session.hasAlreadySynced()
             networkConnectivityChecker = sessionComponent.networkConnectivityChecker()
             taskExecutor = sessionComponent.taskExecutor()
-            if (cancelableTask == null) {
-                timer.cancel()
-                timer = Timer()
-                doSync(true)
+            coroutineDispatchers = sessionComponent.coroutineDispatchers()
+            backgroundDetectionObserver = matrix.backgroundDetectionObserver
+            onStart(isInitialSync)
+            if (isRunning.get()) {
+                Timber.i("Received a start while was already syncing... ignore")
             } else {
-                // Already syncing ignore
-                Timber.i("Received a start while was already syncking... ignore")
+                isRunning.set(true)
+                serviceScope.launch(coroutineDispatchers.io) {
+                    doSync()
+                }
             }
         }
         // No intent just start the service, an alarm will should call with intent
@@ -75,98 +85,61 @@ open class SyncService : Service() {
 
     override fun onDestroy() {
         Timber.i("## onDestroy() : $this")
-
         if (!mIsSelfDestroyed) {
             Timber.w("## Destroy by the system : $this")
         }
-
-        cancelableTask?.cancel()
+        serviceScope.coroutineContext.cancelChildren()
+        isRunning.set(false)
         super.onDestroy()
     }
 
-    fun stopMe() {
-        timer.cancel()
-        timer = Timer()
-        cancelableTask?.cancel()
+    private fun stopMe() {
         mIsSelfDestroyed = true
         stopSelf()
     }
 
-    fun doSync(once: Boolean = false) {
-        if (!networkConnectivityChecker.hasInternetAccess) {
-            Timber.v("No internet access. Waiting...")
-            // TODO Retry in ?
-            timer.schedule(object : TimerTask() {
-                override fun run() {
-                    doSync()
-                }
-            }, NO_NETWORK_DELAY)
-        } else {
-            Timber.v("Execute sync request with timeout 0")
-            val params = SyncTask.Params(TIME_OUT)
-            cancelableTask = syncTask
-                    .configureWith(params) {
-                        callbackThread = TaskThread.SYNC
-                        executionThread = TaskThread.SYNC
-                        callback = object : MatrixCallback {
-                            override fun onSuccess(data: Unit) {
-                                cancelableTask = null
-                                if (!once) {
-                                    timer.schedule(object : TimerTask() {
-                                        override fun run() {
-                                            doSync()
-                                        }
-                                    }, NEXT_BATCH_DELAY)
-                                } else {
-                                    // stop
-                                    stopMe()
-                                }
-                            }
-
-                            override fun onFailure(failure: Throwable) {
-                                Timber.e(failure)
-                                cancelableTask = null
-                                if (failure is Failure.NetworkConnection
-                                        && failure.cause is SocketTimeoutException) {
-                                    // Timeout are not critical
-                                    timer.schedule(object : TimerTask() {
-                                        override fun run() {
-                                            doSync()
-                                        }
-                                    }, 5_000L)
-                                }
-
-                                if (failure !is Failure.NetworkConnection
-                                        || failure.cause is JsonEncodingException) {
-                                    // Wait 10s before retrying
-                                    timer.schedule(object : TimerTask() {
-                                        override fun run() {
-                                            doSync()
-                                        }
-                                    }, 5_000L)
-                                }
-
-                                if (failure is Failure.ServerError
-                                        && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
-                                    // No token or invalid token, stop the thread
-                                    stopSelf()
-                                }
-                            }
-                        }
-                    }
-                    .executeBy(taskExecutor)
+    private suspend fun doSync() {
+        if (!networkConnectivityChecker.hasInternetAccess()) {
+            Timber.v("No network reschedule to avoid wasting resources")
+            userId?.also {
+                onRescheduleAsked(it, isInitialSync, delay = 10_000L)
+            }
+            stopMe()
+            return
+        }
+        Timber.v("Execute sync request with timeout 0")
+        val params = SyncTask.Params(TIME_OUT)
+        try {
+            syncTask.execute(params)
+            // Start sync if we were doing an initial sync and the syncThread is not launched yet
+            if (isInitialSync && session.getSyncStateLive().value == SyncState.Idle) {
+                val isForeground = !backgroundDetectionObserver.isInBackground
+                session.startSync(isForeground)
+            }
+            stopMe()
+        } catch (throwable: Throwable) {
+            Timber.e(throwable)
+            if (throwable.isTokenError()) {
+                stopMe()
+            } else {
+                Timber.v("Retry to sync in 5s")
+                delay(DELAY_FAILURE)
+                doSync()
+            }
         }
     }
 
+    abstract fun onStart(isInitialSync: Boolean)
+
+    abstract fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long)
+
     override fun onBind(intent: Intent?): IBinder? {
         return null
     }
 
     companion object {
         const val EXTRA_USER_ID = "EXTRA_USER_ID"
-
-        const val TIME_OUT = 0L
-        const val NEXT_BATCH_DELAY = 60_000L
-        const val NO_NETWORK_DELAY = 5_000L
+        private const val TIME_OUT = 0L
+        private const val DELAY_FAILURE = 5_000L
     }
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt
index 69e03c6269..c550d40e1e 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt
@@ -19,20 +19,15 @@ package im.vector.matrix.android.internal.session.sync.job
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import com.squareup.moshi.JsonEncodingException
-import im.vector.matrix.android.api.MatrixCallback
 import im.vector.matrix.android.api.failure.Failure
-import im.vector.matrix.android.api.failure.MatrixError
+import im.vector.matrix.android.api.failure.isTokenError
 import im.vector.matrix.android.api.session.sync.SyncState
-import im.vector.matrix.android.api.util.Cancelable
 import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
 import im.vector.matrix.android.internal.session.sync.SyncTask
-import im.vector.matrix.android.internal.task.TaskExecutor
-import im.vector.matrix.android.internal.task.TaskThread
-import im.vector.matrix.android.internal.task.configureWith
 import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
+import kotlinx.coroutines.*
 import timber.log.Timber
 import java.net.SocketTimeoutException
-import java.util.concurrent.CountDownLatch
 import javax.inject.Inject
 
 private const val RETRY_WAIT_TIME_MS = 10_000L
@@ -40,14 +35,13 @@ private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
 
 internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
                                               private val networkConnectivityChecker: NetworkConnectivityChecker,
-                                              private val backgroundDetectionObserver: BackgroundDetectionObserver,
-                                              private val taskExecutor: TaskExecutor
-) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
+                                              private val backgroundDetectionObserver: BackgroundDetectionObserver)
+    : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
 
     private var state: SyncState = SyncState.Idle
     private var liveState = MutableLiveData()
     private val lock = Object()
-    private var cancelableTask: Cancelable? = null
+    private val syncScope = CoroutineScope(SupervisorJob())
 
     private var isStarted = false
     private var isTokenValid = true
@@ -75,14 +69,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
         if (isStarted) {
             Timber.v("Pause sync...")
             isStarted = false
-            cancelableTask?.cancel()
+            syncScope.coroutineContext.cancelChildren()
         }
     }
 
     fun kill() = synchronized(lock) {
         Timber.v("Kill sync...")
         updateStateTo(SyncState.Killing)
-        cancelableTask?.cancel()
+        syncScope.coroutineContext.cancelChildren()
         lock.notify()
     }
 
@@ -102,11 +96,9 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
         isStarted = true
         networkConnectivityChecker.register(this)
         backgroundDetectionObserver.register(this)
-
         while (state != SyncState.Killing) {
             Timber.v("Entering loop, state: $state")
-
-            if (!networkConnectivityChecker.hasInternetAccess) {
+            if (!networkConnectivityChecker.hasInternetAccess()) {
                 Timber.v("No network. Waiting...")
                 updateStateTo(SyncState.NoNetwork)
                 synchronized(lock) { lock.wait() }
@@ -125,58 +117,16 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
                 if (state !is SyncState.Running) {
                     updateStateTo(SyncState.Running(afterPause = true))
                 }
-
                 // No timeout after a pause
                 val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
-
                 Timber.v("Execute sync request with timeout $timeout")
-                val latch = CountDownLatch(1)
                 val params = SyncTask.Params(timeout)
-
-                cancelableTask = syncTask.configureWith(params) {
-                    this.callbackThread = TaskThread.SYNC
-                    this.executionThread = TaskThread.SYNC
-                    this.callback = object : MatrixCallback {
-                        override fun onSuccess(data: Unit) {
-                            Timber.v("onSuccess")
-                            latch.countDown()
-                        }
-
-                        override fun onFailure(failure: Throwable) {
-                            if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) {
-                                // Timeout are not critical
-                                Timber.v("Timeout")
-                            } else if (failure is Failure.Cancelled) {
-                                Timber.v("Cancelled")
-                            } else if (failure is Failure.ServerError
-                                    && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
-                                // No token or invalid token
-                                Timber.w(failure)
-                                isTokenValid = false
-                                isStarted = false
-                            } else {
-                                Timber.e(failure)
-
-                                if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) {
-                                    // Wait 10s before retrying
-                                    Timber.v("Wait 10s")
-                                    sleep(RETRY_WAIT_TIME_MS)
-                                }
-                            }
-
-                            latch.countDown()
-                        }
-                    }
+                val sync = syncScope.launch {
+                    doSync(params)
                 }
-                        .executeBy(taskExecutor)
-
-                latch.await()
-                state.let {
-                    if (it is SyncState.Running && it.afterPause) {
-                        updateStateTo(SyncState.Running(afterPause = false))
-                    }
+                runBlocking {
+                    sync.join()
                 }
-
                 Timber.v("...Continue")
             }
         }
@@ -186,6 +136,37 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
         networkConnectivityChecker.unregister(this)
     }
 
+    private suspend fun doSync(params: SyncTask.Params) {
+        try {
+            syncTask.execute(params)
+        } catch (failure: Throwable) {
+            if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) {
+                // Timeout are not critical
+                Timber.v("Timeout")
+            } else if (failure is Failure.Cancelled) {
+                Timber.v("Cancelled")
+            } else if (failure.isTokenError()) {
+                // No token or invalid token, stop the thread
+                Timber.w(failure)
+                isStarted = false
+                isTokenValid = false
+            } else {
+                Timber.e(failure)
+                if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) {
+                    // Wait 10s before retrying
+                    Timber.v("Wait 10s")
+                    delay(RETRY_WAIT_TIME_MS)
+                }
+            }
+        } finally {
+            state.let {
+                if (it is SyncState.Running && it.afterPause) {
+                    updateStateTo(SyncState.Running(afterPause = false))
+                }
+            }
+        }
+    }
+
     private fun updateStateTo(newState: SyncState) {
         Timber.v("Update state from $state to $newState")
         state = newState
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt
index b5177172d0..eb4f2ff7c2 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt
@@ -18,14 +18,14 @@ package im.vector.matrix.android.internal.session.sync.job
 import android.content.Context
 import androidx.work.*
 import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.api.failure.isTokenError
+import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
 import im.vector.matrix.android.internal.session.sync.SyncTask
 import im.vector.matrix.android.internal.task.TaskExecutor
-import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
 import im.vector.matrix.android.internal.worker.WorkManagerUtil
 import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
 import im.vector.matrix.android.internal.worker.WorkerParamsFactory
 import im.vector.matrix.android.internal.worker.getSessionComponent
-import kotlinx.coroutines.withContext
 import timber.log.Timber
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
@@ -45,46 +45,58 @@ internal class SyncWorker(context: Context,
 
     @Inject lateinit var syncTask: SyncTask
     @Inject lateinit var taskExecutor: TaskExecutor
-    @Inject lateinit var coroutineDispatchers: MatrixCoroutineDispatchers
+    @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker
 
     override suspend fun doWork(): Result {
         Timber.i("Sync work starting")
         val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success()
         val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
         sessionComponent.inject(this)
-        runCatching {
-            withContext(coroutineDispatchers.sync) {
-                val taskParams = SyncTask.Params(0)
-                syncTask.execute(taskParams)
-            }
-        }
-        return Result.success()
+        return runCatching {
+            doSync(params.timeout)
+        }.fold(
+                { Result.success() },
+                { failure ->
+                    if (failure.isTokenError() || !params.automaticallyRetry) {
+                        Result.failure()
+                    } else {
+                        Result.retry()
+                    }
+                }
+        )
+    }
+
+    private suspend fun doSync(timeout: Long) {
+        val taskParams = SyncTask.Params(timeout)
+        syncTask.execute(taskParams)
     }
 
     companion object {
 
+        const val BG_SYNC_WORK_NAME = "BG_SYNCP"
+
         fun requireBackgroundSync(context: Context, userId: String, serverTimeout: Long = 0) {
             val data = WorkerParamsFactory.toData(Params(userId, serverTimeout, false))
             val workRequest = matrixOneTimeWorkRequestBuilder()
-                    .setInputData(data)
                     .setConstraints(WorkManagerUtil.workConstraints)
                     .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
+                    .setInputData(data)
                     .build()
-            WorkManager.getInstance(context).enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest)
+            WorkManager.getInstance(context).enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest)
         }
 
         fun automaticallyBackgroundSync(context: Context, userId: String, serverTimeout: Long = 0, delay: Long = 30_000) {
             val data = WorkerParamsFactory.toData(Params(userId, serverTimeout, true))
             val workRequest = matrixOneTimeWorkRequestBuilder()
-                    .setInputData(data)
                     .setConstraints(WorkManagerUtil.workConstraints)
+                    .setInputData(data)
                     .setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS)
                     .build()
-            WorkManager.getInstance(context).enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest)
+            WorkManager.getInstance(context).enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest)
         }
 
         fun stopAnyBackgroundSync(context: Context) {
-            WorkManager.getInstance(context).cancelUniqueWork("BG_SYNCP")
+            WorkManager.getInstance(context).cancelUniqueWork(BG_SYNC_WORK_NAME)
         }
     }
 }
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/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt
new file mode 100644
index 0000000000..7062c63816
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.task
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.Channel
+import java.util.concurrent.Executors
+
+/**
+ * This class intends to be used for ensure suspendable methods are played sequentially all the way long.
+ */
+internal interface CoroutineSequencer {
+    /**
+     * @param block the suspendable block to execute
+     * @return the result of the block
+     */
+    suspend fun post(block: suspend () -> T): T
+
+    /**
+     * Cancel all and close, so you won't be able to post anything else after
+     */
+    fun close()
+}
+
+internal open class ChannelCoroutineSequencer : CoroutineSequencer {
+
+    private data class Message(
+            val block: suspend () -> T,
+            val deferred: CompletableDeferred
+    )
+
+    private var messageChannel: Channel> = Channel()
+    private val coroutineScope = CoroutineScope(SupervisorJob())
+    // This will ensure
+    private val singleDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+
+    init {
+        launchCoroutine()
+    }
+
+    private fun launchCoroutine() {
+        coroutineScope.launch(singleDispatcher) {
+            for (message in messageChannel) {
+                try {
+                    val result = message.block()
+                    message.deferred.complete(result)
+                } catch (exception: Throwable) {
+                    message.deferred.completeExceptionally(exception)
+                }
+            }
+        }
+    }
+
+    override fun close() {
+        coroutineScope.coroutineContext.cancelChildren()
+        messageChannel.close()
+    }
+
+    override suspend fun post(block: suspend () -> T): T {
+        val deferred = CompletableDeferred()
+        val message = Message(block, deferred)
+        messageChannel.send(message)
+        return try {
+            deferred.await()
+        } catch (cancellation: CancellationException) {
+            // In case of cancellation, we stop the current coroutine context
+            // and relaunch one to consume next messages
+            coroutineScope.coroutineContext.cancelChildren()
+            launchCoroutine()
+            throw cancellation
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt
index d5392779d1..244cc83901 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt
@@ -85,6 +85,5 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers
         TaskThread.IO          -> coroutineDispatchers.io
         TaskThread.CALLER      -> EmptyCoroutineContext
         TaskThread.CRYPTO      -> coroutineDispatchers.crypto
-        TaskThread.SYNC        -> coroutineDispatchers.sync
     }
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt
index 16ed93662c..c04e9fbce6 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt
@@ -21,6 +21,5 @@ internal enum class TaskThread {
     COMPUTATION,
     IO,
     CALLER,
-    CRYPTO,
-    SYNC
+    CRYPTO
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt
index d89b732a0d..b8de50b9fc 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt
@@ -29,7 +29,7 @@ import javax.inject.Inject
 @MatrixScope
 internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver {
 
-    var isIsBackground: Boolean = false
+    var isInBackground: Boolean = false
         private set
 
     private
@@ -46,14 +46,14 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse
     @OnLifecycleEvent(Lifecycle.Event.ON_START)
     fun onMoveToForeground() {
         Timber.v("App returning to foreground…")
-        isIsBackground = false
+        isInBackground = false
         listeners.forEach { it.onMoveToForeground() }
     }
 
     @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
     fun onMoveToBackground() {
         Timber.v("App going to background…")
-        isIsBackground = true
+        isInBackground = true
         listeners.forEach { it.onMoveToBackground() }
     }
 
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt
index 23201c084e..d15389f703 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt
@@ -22,6 +22,5 @@ internal data class MatrixCoroutineDispatchers(
         val io: CoroutineDispatcher,
         val computation: CoroutineDispatcher,
         val main: CoroutineDispatcher,
-        val crypto: CoroutineDispatcher,
-        val sync: CoroutineDispatcher
+        val crypto: CoroutineDispatcher
 )
diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
index 03bc6d3684..84aa9d9f5c 100644
--- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
+++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
@@ -2,6 +2,19 @@
 
 
 
+    
+        %1$s added %2$s as an address for this room.
+        %1$s added %2$s as addresses for this room.
+    
 
+    
+        %1$s removed %2$s as an address for this room.
+        %1$s removed %3$s as addresses for this room.
+    
 
-
\ No newline at end of file
+    %1$s added %2$s and removed %3$s as addresses for this room.
+
+    "%1$s set the main address for this room to %2$s."
+    "%1$s removed the main address for this room."
+
+
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/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt
new file mode 100644
index 0000000000..9591feaa32
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.task
+
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.util.concurrent.Executors
+
+class CoroutineSequencersTest {
+
+    private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+
+    @Test
+    fun sequencer_should_run_sequential() {
+        val sequencer = ChannelCoroutineSequencer()
+        val results = ArrayList()
+
+        val jobs = listOf(
+                GlobalScope.launch(dispatcher) {
+                    sequencer.post { suspendingMethod("#1") }.also {
+                        results.add(it)
+                    }
+                },
+                GlobalScope.launch(dispatcher) {
+                    sequencer.post { suspendingMethod("#2") }.also {
+                        results.add(it)
+                    }
+                },
+                GlobalScope.launch(dispatcher) {
+                    sequencer.post { suspendingMethod("#3") }.also {
+                        results.add(it)
+                    }
+                }
+        )
+        runBlocking {
+            jobs.joinAll()
+        }
+        assertEquals(3, results.size)
+        assertEquals(results[0], "#1")
+        assertEquals(results[1], "#2")
+        assertEquals(results[2], "#3")
+    }
+
+    @Test
+    fun sequencer_should_run_parallel() {
+        val sequencer1 = ChannelCoroutineSequencer()
+        val sequencer2 = ChannelCoroutineSequencer()
+        val sequencer3 = ChannelCoroutineSequencer()
+        val results = ArrayList()
+        val jobs = listOf(
+                GlobalScope.launch(dispatcher) {
+                    sequencer1.post { suspendingMethod("#1") }.also {
+                        results.add(it)
+                    }
+                },
+                GlobalScope.launch(dispatcher) {
+                    sequencer2.post { suspendingMethod("#2") }.also {
+                        results.add(it)
+                    }
+                },
+                GlobalScope.launch(dispatcher) {
+                    sequencer3.post { suspendingMethod("#3") }.also {
+                        results.add(it)
+                    }
+                }
+        )
+        runBlocking {
+            jobs.joinAll()
+        }
+        assertEquals(3, results.size)
+    }
+
+    @Test
+    fun sequencer_should_jump_to_next_when_current_job_canceled() {
+        val sequencer = ChannelCoroutineSequencer()
+        val results = ArrayList()
+        val jobs = listOf(
+                GlobalScope.launch(dispatcher) {
+                    sequencer.post { suspendingMethod("#1") }.also {
+                        results.add(it)
+                    }
+                },
+                GlobalScope.launch(dispatcher) {
+                    val result = sequencer.post { suspendingMethod("#2") }.also {
+                        results.add(it)
+                    }
+                    println("Result: $result")
+                },
+                GlobalScope.launch(dispatcher) {
+                    sequencer.post { suspendingMethod("#3") }.also {
+                        results.add(it)
+                    }
+                }
+        )
+        // We are canceling the second job
+        jobs[1].cancel()
+        runBlocking {
+            jobs.joinAll()
+        }
+        assertEquals(2, results.size)
+    }
+
+    private suspend fun suspendingMethod(name: String): String {
+        println("BLOCKING METHOD $name STARTS on ${Thread.currentThread().name}")
+        delay(1000)
+        println("BLOCKING METHOD $name ENDS on ${Thread.currentThread().name}")
+        return name
+    }
+}
diff --git a/vector/build.gradle b/vector/build.gradle
index c8d474088f..9bab8596b1 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -186,6 +186,7 @@ android {
         gplay {
             dimension "store"
 
+            resValue "bool", "isGplay", "true"
             buildConfigField "boolean", "ALLOW_FCM_USE", "true"
             buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\""
             buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\""
@@ -194,6 +195,7 @@ android {
         fdroid {
             dimension "store"
 
+            resValue "bool", "isGplay", "false"
             buildConfigField "boolean", "ALLOW_FCM_USE", "false"
             buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\""
             buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\""
@@ -216,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'
@@ -225,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")
@@ -236,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"
@@ -249,9 +251,6 @@ dependencies {
     implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
     kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
 
-    // OSS License
-    implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
-
     // Log
     implementation 'com.jakewharton.timber:timber:4.7.1'
 
@@ -276,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"
@@ -289,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"
@@ -343,6 +342,9 @@ dependencies {
         exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
     }
 
+    // OSS License, gplay flavor only
+    gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
+
     implementation "androidx.emoji:emoji-appcompat:1.0.0"
 
     // TESTS
diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml
index 1a35caff41..32cdba9251 100644
--- a/vector/src/fdroid/AndroidManifest.xml
+++ b/vector/src/fdroid/AndroidManifest.xml
@@ -4,7 +4,6 @@
 
     
     
-    
 
     
 
@@ -20,10 +19,6 @@
             android:enabled="true"
             android:exported="false" />
 
-        
-
     
 
 
\ No newline at end of file
diff --git a/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt b/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt
new file mode 100644
index 0000000000..de1ee2290b
--- /dev/null
+++ b/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.riotx
+
+import android.content.Context
+
+// No op
+fun openOssLicensesMenuActivity(@Suppress("UNUSED_PARAMETER") context: Context) = Unit
diff --git a/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt
index 6648d79490..951fcaa14d 100644
--- a/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt
+++ b/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt
@@ -25,7 +25,7 @@ import android.os.Build
 import android.os.PowerManager
 import androidx.core.content.ContextCompat
 import im.vector.matrix.android.internal.session.sync.job.SyncService
-import im.vector.riotx.fdroid.service.VectorSyncService
+import im.vector.riotx.core.services.VectorSyncService
 import timber.log.Timber
 
 class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
@@ -41,14 +41,9 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
         val userId = intent.getStringExtra(SyncService.EXTRA_USER_ID)
         // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
         Timber.d("RestartBroadcastReceiver received intent")
-        Intent(context, VectorSyncService::class.java).also {
-            it.putExtra(SyncService.EXTRA_USER_ID, userId)
+        VectorSyncService.newIntent(context, userId).also {
             try {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                    ContextCompat.startForegroundService(context, it)
-                } else {
-                    context.startService(it)
-                }
+                ContextCompat.startForegroundService(context, it)
             } catch (ex: Throwable) {
                 // TODO
                 Timber.e(ex)
@@ -79,6 +74,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
         }
 
         fun cancelAlarm(context: Context) {
+            Timber.v("Cancel alarm")
             val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
             val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
             val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
diff --git a/vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt b/vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt
deleted file mode 100644
index fbf9c1a031..0000000000
--- a/vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt
+++ /dev/null
@@ -1,59 +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.fdroid.service
-
-import android.app.NotificationManager
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import im.vector.matrix.android.internal.session.sync.job.SyncService
-import im.vector.riotx.R
-import im.vector.riotx.core.extensions.vectorComponent
-import im.vector.riotx.features.notifications.NotificationUtils
-import timber.log.Timber
-
-class VectorSyncService : SyncService() {
-
-    private lateinit var notificationUtils: NotificationUtils
-
-    override fun onCreate() {
-        super.onCreate()
-        notificationUtils = vectorComponent().notificationUtils()
-    }
-
-    override fun onDestroy() {
-        removeForegroundNotif()
-        super.onDestroy()
-    }
-
-    private fun removeForegroundNotif() {
-        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-        notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE)
-    }
-
-    /**
-     * Service is started only in fdroid mode when no FCM is available
-     * Otherwise it is bounded
-     */
-    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
-        Timber.v("VectorSyncService - onStartCommand ")
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            val notification = notificationUtils.buildForegroundServiceNotification(R.string.notification_listening_for_events, false)
-            startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
-        }
-        return super.onStartCommand(intent, flags, startId)
-    }
-}
diff --git a/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt b/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt
new file mode 100644
index 0000000000..109e9bc978
--- /dev/null
+++ b/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.riotx
+
+import android.content.Context
+import android.content.Intent
+import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
+
+fun openOssLicensesMenuActivity(context: Context) = context.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index ec9fb41275..fe03efecd5 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -5,6 +5,8 @@
 
     
     
+    
+    
 
     
 
+        
+
         
 
         
diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
index 0479449e9a..9a281e5728 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.grouplist.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/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
index b6b0d16360..c76af027ba 100644
--- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
@@ -42,7 +42,7 @@ import im.vector.riotx.core.di.DaggerVectorComponent
 import im.vector.riotx.core.di.HasVectorInjector
 import im.vector.riotx.core.di.VectorComponent
 import im.vector.riotx.core.extensions.configureAndStart
-import im.vector.riotx.core.rx.setupRxPlugin
+import im.vector.riotx.core.rx.RxConfig
 import im.vector.riotx.features.configuration.VectorConfiguration
 import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
 import im.vector.riotx.features.notifications.NotificationDrawerManager
@@ -75,6 +75,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
     @Inject lateinit var versionProvider: VersionProvider
     @Inject lateinit var notificationUtils: NotificationUtils
     @Inject lateinit var appStateHandler: AppStateHandler
+    @Inject lateinit var rxConfig: RxConfig
     lateinit var vectorComponent: VectorComponent
     private var fontThreadHandler: Handler? = null
 
@@ -84,7 +85,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
         vectorComponent = DaggerVectorComponent.factory().create(this)
         vectorComponent.inject(this)
         vectorUncaughtExceptionHandler.activate(this)
-        setupRxPlugin()
+        rxConfig.setupRxPlugin()
 
         if (BuildConfig.DEBUG) {
             Timber.plant(Timber.DebugTree())
@@ -116,11 +117,12 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
         if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
             val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
             activeSessionHolder.setActiveSession(lastAuthenticatedSession)
-            lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener, sessionListener)
+            lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
         }
         ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
             @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
             fun entersForeground() {
+                Timber.i("App entered foreground")
                 FcmHelper.onEnterForeground(appContext)
                 activeSessionHolder.getSafeActiveSession()?.also {
                     it.stopAnyBackgroundSync()
diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
index dcca33a645..9c340fbcb2 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
@@ -47,6 +47,7 @@ import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFr
 import im.vector.riotx.features.roomprofile.RoomProfileFragment
 import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
 import im.vector.riotx.features.settings.*
+import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
 import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
 import im.vector.riotx.features.settings.push.PushGatewaysFragment
 import im.vector.riotx.features.signout.soft.SoftLogoutFragment
@@ -230,6 +231,11 @@ interface FragmentModule {
     @FragmentKey(VectorSettingsIgnoredUsersFragment::class)
     fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment
 
+    @Binds
+    @IntoMap
+    @FragmentKey(VectorSettingsDevicesFragment::class)
+    fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
+
     @Binds
     @IntoMap
     @FragmentKey(SASVerificationIncomingFragment::class)
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 96e97dd86a..2463b577a6 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 27bff7e11e..f553513bfa 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 is at least started? $isAtLeastStarted")
-    startSync(isAtLeastStarted)
+    startSyncing(context)
     refreshPushers()
     pushRuleTriggerListener.startWithSession(this)
 
@@ -42,6 +44,24 @@ fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener,
     // @Inject lateinit var keyRequestHandler: KeyRequestHandler
 }
 
+fun Session.startSyncing(context: Context) {
+    val applicationContext = context.applicationContext
+    if (!hasAlreadySynced()) {
+        VectorSyncService.newIntent(applicationContext, myUserId).also {
+            try {
+                ContextCompat.startForegroundService(applicationContext, it)
+            } catch (ex: Throwable) {
+                // TODO
+                Timber.e(ex)
+            }
+        }
+    } else {
+        val isAtLeastStarted = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
+        Timber.v("--> is at least started? $isAtLeastStarted")
+        startSync(isAtLeastStarted)
+    }
+}
+
 /**
  * Tell is the session has unsaved e2e keys in the backup
  */
diff --git a/vector/src/main/java/im/vector/riotx/core/hardware/vibrator.kt b/vector/src/main/java/im/vector/riotx/core/hardware/vibrator.kt
new file mode 100644
index 0000000000..8fc24d7fff
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/hardware/vibrator.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.riotx.core.hardware
+
+import android.content.Context
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.Vibrator
+
+fun vibrate(context: Context, durationMillis: Long = 100) {
+    val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        vibrator.vibrate(VibrationEffect.createOneShot(durationMillis, VibrationEffect.DEFAULT_AMPLITUDE))
+    } else {
+        @Suppress("DEPRECATION")
+        vibrator.vibrate(durationMillis)
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
index 0443c60af9..40ce65e3ef 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
@@ -54,6 +54,7 @@ import im.vector.riotx.features.rageshake.BugReportActivity
 import im.vector.riotx.features.rageshake.BugReporter
 import im.vector.riotx.features.rageshake.RageShake
 import im.vector.riotx.features.session.SessionListener
+import im.vector.riotx.features.settings.VectorPreferences
 import im.vector.riotx.features.themes.ActivityOtherThemes
 import im.vector.riotx.features.themes.ThemeUtils
 import im.vector.riotx.receivers.DebugReceiver
@@ -88,9 +89,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
     private lateinit var configurationViewModel: ConfigurationViewModel
     private lateinit var sessionListener: SessionListener
     protected lateinit var bugReporter: BugReporter
-    private lateinit var rageShake: RageShake
+    lateinit var rageShake: RageShake
+        private set
     protected lateinit var navigator: Navigator
     private lateinit var activeSessionHolder: ActiveSessionHolder
+    private lateinit var vectorPreferences: VectorPreferences
 
     // Filter for multiple invalid token error
     private var mainActivityStarted = false
@@ -135,7 +138,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {
-        screenComponent = DaggerScreenComponent.factory().create(getVectorComponent(), this)
+        val vectorComponent = getVectorComponent()
+        screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
         val timeForInjection = measureTimeMillis {
             injectWith(screenComponent)
         }
@@ -150,6 +154,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
         rageShake = screenComponent.rageShake()
         navigator = screenComponent.navigator()
         activeSessionHolder = screenComponent.activeSessionHolder()
+        vectorPreferences = vectorComponent.vectorPreferences()
         configurationViewModel.activityRestarter.observe(this, Observer {
             if (!it.hasBeenHandled) {
                 // Recreate the Activity because configuration has changed
@@ -226,7 +231,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
 
         configurationViewModel.onActivityResumed()
 
-        if (this !is BugReportActivity) {
+        if (this !is BugReportActivity && vectorPreferences.useRageshake()) {
             rageShake.start()
         }
 
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
index 8e1ad72150..3bb667593c 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
@@ -31,6 +31,7 @@ import butterknife.Unbinder
 import com.airbnb.mvrx.BaseMvRxFragment
 import com.airbnb.mvrx.MvRx
 import com.bumptech.glide.util.Util.assertMainThread
+import com.google.android.material.snackbar.Snackbar
 import im.vector.riotx.core.di.DaggerScreenComponent
 import im.vector.riotx.core.di.HasScreenInjector
 import im.vector.riotx.core.di.ScreenComponent
@@ -167,6 +168,13 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
         return this
     }
 
+    protected fun showErrorInSnackbar(throwable: Throwable) {
+        vectorBaseActivity.coordinatorLayout?.let {
+            Snackbar.make(it, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
+                    .show()
+        }
+    }
+
     /* ==========================================================================================
      * Toolbar
      * ========================================================================================== */
diff --git a/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt b/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt
deleted file mode 100644
index f9f9da644b..0000000000
--- a/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2018 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.core.preference
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.preference.Preference
-import im.vector.riotx.R
-
-/**
- * Divider for Preference screen
- */
-class VectorPreferenceDivider @JvmOverloads constructor(context: Context,
-                                                        attrs: AttributeSet? = null,
-                                                        defStyleAttr: Int = 0,
-                                                        defStyleRes: Int = 0
-) : Preference(context, attrs, defStyleAttr, defStyleRes) {
-
-    init {
-        layoutResource = R.layout.vector_preference_divider
-    }
-}
diff --git a/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt b/vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt
similarity index 57%
rename from vector/src/main/java/im/vector/riotx/core/rx/Rx.kt
rename to vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt
index 89de9030dc..d8828eb1b8 100644
--- a/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt
+++ b/vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt
@@ -17,19 +17,26 @@
 package im.vector.riotx.core.rx
 
 import im.vector.riotx.BuildConfig
+import im.vector.riotx.features.settings.VectorPreferences
 import io.reactivex.plugins.RxJavaPlugins
 import timber.log.Timber
+import javax.inject.Inject
 
-/**
- * Make sure unhandled Rx error does not crash the app in production
- */
-fun setupRxPlugin() {
-    RxJavaPlugins.setErrorHandler { throwable ->
-        Timber.e(throwable, "RxError")
+class RxConfig @Inject constructor(
+        private val vectorPreferences: VectorPreferences
+) {
 
-        // Avoid crash in production
-        if (BuildConfig.DEBUG) {
-            throw throwable
+    /**
+     * Make sure unhandled Rx error does not crash the app in production
+     */
+    fun setupRxPlugin() {
+        RxJavaPlugins.setErrorHandler { throwable ->
+            Timber.e(throwable, "RxError")
+
+            // Avoid crash in production
+            if (BuildConfig.DEBUG || vectorPreferences.failFast()) {
+                throw throwable
+            }
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt
new file mode 100644
index 0000000000..b6b8fbf06a
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.core.services
+
+import android.app.AlarmManager
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import im.vector.matrix.android.internal.session.sync.job.SyncService
+import im.vector.riotx.R
+import im.vector.riotx.core.extensions.vectorComponent
+import im.vector.riotx.features.notifications.NotificationUtils
+
+class VectorSyncService : SyncService() {
+
+    companion object {
+
+        fun newIntent(context: Context, userId: String): Intent {
+            return Intent(context, VectorSyncService::class.java).also {
+                it.putExtra(EXTRA_USER_ID, userId)
+            }
+        }
+    }
+
+    private lateinit var notificationUtils: NotificationUtils
+
+    override fun onCreate() {
+        super.onCreate()
+        notificationUtils = vectorComponent().notificationUtils()
+    }
+
+    override fun onStart(isInitialSync: Boolean) {
+        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) {
+        reschedule(userId, delay)
+    }
+
+    override fun onDestroy() {
+        removeForegroundNotif()
+        super.onDestroy()
+    }
+
+    private fun removeForegroundNotif() {
+        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+        notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE)
+    }
+
+    private fun reschedule(userId: String, delay: Long) {
+        val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            PendingIntent.getForegroundService(this, 0, newIntent(this, userId), 0)
+        } else {
+            PendingIntent.getService(this, 0, newIntent(this, userId), 0)
+        }
+        val firstMillis = System.currentTimeMillis() + delay
+        val alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)
+        } else {
+            alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt
index b02e3c9366..627d757574 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt
@@ -19,16 +19,14 @@ package im.vector.riotx.core.utils
 
 import android.os.Handler
 
-internal class Debouncer(private val handler: Handler) {
+class Debouncer(private val handler: Handler) {
 
     private val runnables = HashMap()
 
     fun debounce(identifier: String, millis: Long, r: Runnable): Boolean {
-        if (runnables.containsKey(identifier)) {
-            // debounce
-            val old = runnables[identifier]
-            handler.removeCallbacks(old)
-        }
+        // debounce
+        cancel(identifier)
+
         insertRunnable(identifier, r, millis)
         return true
     }
@@ -37,6 +35,14 @@ internal class Debouncer(private val handler: Handler) {
         handler.removeCallbacksAndMessages(null)
     }
 
+    fun cancel(identifier: String) {
+        if (runnables.containsKey(identifier)) {
+            val old = runnables[identifier]
+            handler.removeCallbacks(old)
+            runnables.remove(identifier)
+        }
+    }
+
     private fun insertRunnable(identifier: String, r: Runnable, millis: Long) {
         val chained = Runnable {
             handler.post(r)
diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
index 041eb85a11..26ecd25178 100644
--- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
@@ -28,6 +28,7 @@ import im.vector.riotx.R
 import im.vector.riotx.core.di.ActiveSessionHolder
 import im.vector.riotx.core.di.ScreenComponent
 import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.extensions.startSyncing
 import im.vector.riotx.core.platform.VectorBaseActivity
 import im.vector.riotx.core.utils.deleteAllFiles
 import im.vector.riotx.features.home.HomeActivity
@@ -84,11 +85,9 @@ class MainActivity : VectorBaseActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         args = parseArgs()
-
         if (args.clearCredentials || args.isUserLoggedOut) {
             clearNotifications()
         }
-
         // Handle some wanted cleanup
         if (args.clearCache || args.clearCredentials) {
             doCleanUp()
@@ -116,24 +115,32 @@ class MainActivity : VectorBaseActivity() {
     }
 
     private fun doCleanUp() {
+        val session = sessionHolder.getSafeActiveSession()
+        if (session == null) {
+            startNextActivityAndFinish()
+            return
+        }
         when {
-            args.clearCredentials -> sessionHolder.getActiveSession().signOut(
+            args.clearCredentials -> session.signOut(
                     !args.isUserLoggedOut,
                     object : MatrixCallback {
                         override fun onSuccess(data: Unit) {
                             Timber.w("SIGN_OUT: success, start app")
                             sessionHolder.clearActiveSession()
-                            doLocalCleanupAndStart()
+                            doLocalCleanup()
+                            startNextActivityAndFinish()
                         }
 
                         override fun onFailure(failure: Throwable) {
                             displayError(failure)
                         }
                     })
-            args.clearCache       -> sessionHolder.getActiveSession().clearCache(
+            args.clearCache       -> session.clearCache(
                     object : MatrixCallback {
                         override fun onSuccess(data: Unit) {
-                            doLocalCleanupAndStart()
+                            doLocalCleanup()
+                            session.startSyncing(applicationContext)
+                            startNextActivityAndFinish()
                         }
 
                         override fun onFailure(failure: Throwable) {
@@ -148,7 +155,7 @@ class MainActivity : VectorBaseActivity() {
         Timber.w("Ignoring invalid token global error")
     }
 
-    private fun doLocalCleanupAndStart() {
+    private fun doLocalCleanup() {
         GlobalScope.launch(Dispatchers.Main) {
             // On UI Thread
             Glide.get(this@MainActivity).clearMemory()
@@ -160,8 +167,6 @@ class MainActivity : VectorBaseActivity() {
                 deleteAllFiles(this@MainActivity.cacheDir)
             }
         }
-
-        startNextActivityAndFinish()
     }
 
     private fun displayError(failure: Throwable) {
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/AutocompleteMatrixItem.kt
similarity index 71%
rename from vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt
rename to vector/src/main/java/im/vector/riotx/features/autocomplete/AutocompleteMatrixItem.kt
index 8581ba8e2c..d5eb90a62c 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/AutocompleteMatrixItem.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package im.vector.riotx.features.autocomplete.user
+package im.vector.riotx.features.autocomplete
 
 import android.view.View
 import android.widget.ImageView
@@ -25,23 +25,27 @@ import im.vector.matrix.android.api.util.MatrixItem
 import im.vector.riotx.R
 import im.vector.riotx.core.epoxy.VectorEpoxyHolder
 import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.extensions.setTextOrHide
 import im.vector.riotx.features.home.AvatarRenderer
 
-@EpoxyModelClass(layout = R.layout.item_autocomplete_user)
-abstract class AutocompleteUserItem : VectorEpoxyModel() {
+@EpoxyModelClass(layout = R.layout.item_autocomplete_matrix_item)
+abstract class AutocompleteMatrixItem : VectorEpoxyModel() {
 
     @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
     @EpoxyAttribute lateinit var matrixItem: MatrixItem
+    @EpoxyAttribute var subName: String? = null
     @EpoxyAttribute var clickListener: View.OnClickListener? = null
 
     override fun bind(holder: Holder) {
         holder.view.setOnClickListener(clickListener)
         holder.nameView.text = matrixItem.getBestName()
+        holder.subNameView.setTextOrHide(subName)
         avatarRenderer.render(matrixItem, holder.avatarImageView)
     }
 
     class Holder : VectorEpoxyHolder() {
-        val nameView by bind(R.id.userAutocompleteName)
-        val avatarImageView by bind(R.id.userAutocompleteAvatar)
+        val nameView by bind(R.id.matrixItemAutocompleteName)
+        val subNameView by bind(R.id.matrixItemAutocompleteSubname)
+        val avatarImageView by bind(R.id.matrixItemAutocompleteAvatar)
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt
deleted file mode 100644
index 227f1b2f9c..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt
+++ /dev/null
@@ -1,91 +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
-
-import android.content.Context
-import android.database.DataSetObserver
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import com.airbnb.epoxy.EpoxyController
-import com.airbnb.epoxy.EpoxyRecyclerView
-import com.otaliastudios.autocomplete.AutocompletePresenter
-
-abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePresenter(context), AutocompleteClickListener {
-
-    private var recyclerView: EpoxyRecyclerView? = null
-    private var clicks: AutocompletePresenter.ClickProvider? = null
-    private var observer: Observer? = null
-
-    override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider) {
-        this.clicks = provider
-    }
-
-    override fun registerDataSetObserver(observer: DataSetObserver) {
-        this.observer = Observer(observer)
-    }
-
-    override fun getView(): ViewGroup? {
-        recyclerView = EpoxyRecyclerView(context).apply {
-            setController(providesController())
-            observer?.let {
-                adapter?.registerAdapterDataObserver(it)
-            }
-            itemAnimator = null
-        }
-        return recyclerView
-    }
-
-    override fun onViewShown() {}
-
-    override fun onViewHidden() {
-        recyclerView = null
-        observer = null
-    }
-
-    abstract fun providesController(): EpoxyController
-
-    protected fun dispatchLayoutChange() {
-        observer?.onChanged()
-    }
-
-    override fun onItemClick(t: T) {
-        clicks?.click(t)
-    }
-
-    private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() {
-
-        override fun onChanged() {
-            root.onChanged()
-        }
-
-        override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
-            root.onChanged()
-        }
-
-        override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
-            root.onChanged()
-        }
-
-        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
-            root.onChanged()
-        }
-
-        override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
-            root.onChanged()
-        }
-    }
-}
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt
index 915689fbeb..84ae8db217 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt
@@ -17,21 +17,28 @@
 package im.vector.riotx.features.autocomplete.command
 
 import android.content.Context
-import com.airbnb.epoxy.EpoxyController
-import im.vector.riotx.features.autocomplete.EpoxyAutocompletePresenter
+import androidx.recyclerview.widget.RecyclerView
+import com.otaliastudios.autocomplete.RecyclerViewPresenter
+import im.vector.riotx.features.autocomplete.AutocompleteClickListener
 import im.vector.riotx.features.command.Command
 import javax.inject.Inject
 
 class AutocompleteCommandPresenter @Inject constructor(context: Context,
                                                        private val controller: AutocompleteCommandController) :
-        EpoxyAutocompletePresenter(context) {
+        RecyclerViewPresenter(context), AutocompleteClickListener {
 
     init {
         controller.listener = this
     }
 
-    override fun providesController(): EpoxyController {
-        return controller
+    override fun instantiateAdapter(): RecyclerView.Adapter<*> {
+        // Also remove animation
+        recyclerView?.itemAnimator = null
+        return controller.adapter
+    }
+
+    override fun onItemClick(t: Command) {
+        dispatchClick(t)
     }
 
     override fun onQuery(query: CharSequence?) {
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
new file mode 100644
index 0000000000..6d498de2d2
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.emoji
+
+import android.graphics.Typeface
+import androidx.recyclerview.widget.RecyclerView
+import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.riotx.EmojiCompatFontProvider
+import im.vector.riotx.features.autocomplete.AutocompleteClickListener
+import im.vector.riotx.features.reactions.ReactionClickListener
+import im.vector.riotx.features.reactions.data.EmojiItem
+import javax.inject.Inject
+
+class AutocompleteEmojiController @Inject constructor(
+        private val fontProvider: EmojiCompatFontProvider
+) : TypedEpoxyController>() {
+
+    var emojiTypeface: Typeface? = fontProvider.typeface
+
+    private val fontProviderListener = object : EmojiCompatFontProvider.FontProviderListener {
+        override fun compatibilityFontUpdate(typeface: Typeface?) {
+            emojiTypeface = typeface
+        }
+    }
+
+    var listener: AutocompleteClickListener? = null
+
+    override fun buildModels(data: List?) {
+        if (data.isNullOrEmpty()) {
+            return
+        }
+        data
+                .take(MAX)
+                .forEach { emojiItem ->
+                    autocompleteEmojiItem {
+                        id(emojiItem.name)
+                        emojiItem(emojiItem)
+                        emojiTypeFace(emojiTypeface)
+                        onClickListener(
+                                object : ReactionClickListener {
+                                    override fun onReactionSelected(reaction: String) {
+                                        listener?.onItemClick(reaction)
+                                    }
+                                }
+                        )
+                    }
+                }
+
+        if (data.size > MAX) {
+            autocompleteMoreResultItem {
+                id("more_result")
+            }
+        }
+    }
+
+    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
+        fontProvider.addListener(fontProviderListener)
+    }
+
+    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
+        super.onDetachedFromRecyclerView(recyclerView)
+        fontProvider.removeListener(fontProviderListener)
+    }
+
+    companion object {
+        const val MAX = 50
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt
new file mode 100644
index 0000000000..36759f9271
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.emoji
+
+import android.graphics.Typeface
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.extensions.setTextOrHide
+import im.vector.riotx.features.reactions.ReactionClickListener
+import im.vector.riotx.features.reactions.data.EmojiItem
+
+@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji)
+abstract class AutocompleteEmojiItem : VectorEpoxyModel() {
+
+    @EpoxyAttribute
+    lateinit var emojiItem: EmojiItem
+
+    @EpoxyAttribute
+    var emojiTypeFace: Typeface? = null
+
+    @EpoxyAttribute
+    var onClickListener: ReactionClickListener? = null
+
+    override fun bind(holder: Holder) {
+        holder.emojiText.text = emojiItem.emoji
+        holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT
+        holder.emojiNameText.text = emojiItem.name
+        holder.emojiKeywordText.setTextOrHide(emojiItem.keywords.joinToString())
+
+        holder.view.setOnClickListener {
+            onClickListener?.onReactionSelected(emojiItem.emoji)
+        }
+    }
+
+    class Holder : VectorEpoxyHolder() {
+        val emojiText by bind(R.id.itemAutocompleteEmoji)
+        val emojiNameText by bind(R.id.itemAutocompleteEmojiName)
+        val emojiKeywordText by bind(R.id.itemAutocompleteEmojiSubname)
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
new file mode 100644
index 0000000000..731b48af86
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.emoji
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+import com.otaliastudios.autocomplete.RecyclerViewPresenter
+import im.vector.riotx.features.autocomplete.AutocompleteClickListener
+import im.vector.riotx.features.reactions.data.EmojiDataSource
+import javax.inject.Inject
+
+class AutocompleteEmojiPresenter @Inject constructor(context: Context,
+                                                     private val emojiDataSource: EmojiDataSource,
+                                                     private val controller: AutocompleteEmojiController) :
+        RecyclerViewPresenter(context), AutocompleteClickListener {
+
+    init {
+        controller.listener = this
+    }
+
+    override fun instantiateAdapter(): RecyclerView.Adapter<*> {
+        // Also remove animation
+        recyclerView?.itemAnimator = null
+        return controller.adapter
+    }
+
+    override fun onItemClick(t: String) {
+        dispatchClick(t)
+    }
+
+    override fun onQuery(query: CharSequence?) {
+        val data = if (query.isNullOrBlank()) {
+            // Return common emojis
+            emojiDataSource.getQuickReactions()
+        } else {
+            emojiDataSource.filterWith(query.toString())
+        }
+        controller.setData(data)
+    }
+}
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/autocomplete/emoji/AutocompleteMoreResultItem.kt
similarity index 54%
rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt
rename to vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt
index b2cec09096..844cc96035 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package im.vector.riotx.features.home.room.detail.composer
+package im.vector.riotx.features.autocomplete.emoji
 
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.MvRxState
-import com.airbnb.mvrx.Uninitialized
-import im.vector.matrix.android.api.session.user.model.User
-import im.vector.riotx.features.home.room.detail.RoomDetailArgs
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
 
-data class TextComposerViewState(val roomId: String,
-                                 val asyncUsers: Async> = Uninitialized
-) : MvRxState {
+@EpoxyModelClass(layout = R.layout.item_autocomplete_more_result)
+abstract class AutocompleteMoreResultItem : VectorEpoxyModel() {
 
-    constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
+    class Holder : VectorEpoxyHolder()
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt
new file mode 100644
index 0000000000..5d0d43d9ea
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.group
+
+import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.matrix.android.api.session.group.model.GroupSummary
+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 AutocompleteGroupController @Inject constructor() : TypedEpoxyController>() {
+
+    var listener: AutocompleteClickListener? = null
+
+    @Inject lateinit var avatarRenderer: AvatarRenderer
+
+    override fun buildModels(data: List?) {
+        if (data.isNullOrEmpty()) {
+            return
+        }
+        data.forEach { groupSummary ->
+            autocompleteMatrixItem {
+                id(groupSummary.groupId)
+                matrixItem(groupSummary.toMatrixItem())
+                avatarRenderer(avatarRenderer)
+                clickListener { _ ->
+                    listener?.onItemClick(groupSummary)
+                }
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000000..b6f45b477c
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.group
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+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 session: Session
+) : RecyclerViewPresenter(context), AutocompleteClickListener {
+
+    init {
+        controller.listener = this
+    }
+
+    override fun instantiateAdapter(): RecyclerView.Adapter<*> {
+        // Also remove animation
+        recyclerView?.itemAnimator = null
+        return controller.adapter
+    }
+
+    override fun onItemClick(t: GroupSummary) {
+        dispatchClick(t)
+    }
+
+    override fun onQuery(query: CharSequence?) {
+        val queryParams = groupSummaryQueryParams {
+            displayName = if (query.isNullOrBlank()) {
+                QueryStringValue.IsNotEmpty
+            } else {
+                QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+            }
+        }
+        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 73%
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 8f0090001f..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,27 +14,28 @@
  * 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
         }
         data.forEach { user ->
-            autocompleteUserItem {
+            autocompleteMatrixItem {
                 id(user.userId)
                 matrixItem(user.toMatrixItem())
                 avatarRenderer(avatarRenderer)
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
new file mode 100644
index 0000000000..aae95502d9
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.room
+
+import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.matrix.android.api.session.room.model.RoomSummary
+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 AutocompleteRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer) : TypedEpoxyController>() {
+
+    var listener: AutocompleteClickListener? = null
+
+    override fun buildModels(data: List?) {
+        if (data.isNullOrEmpty()) {
+            return
+        }
+        data.forEach { roomSummary ->
+            autocompleteMatrixItem {
+                id(roomSummary.roomId)
+                matrixItem(roomSummary.toMatrixItem())
+                subName(roomSummary.canonicalAlias)
+                avatarRenderer(avatarRenderer)
+                clickListener { _ ->
+                    listener?.onItemClick(roomSummary)
+                }
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000000..17787a22ef
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.room
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+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 session: Session
+) : RecyclerViewPresenter(context), AutocompleteClickListener {
+
+    init {
+        controller.listener = this
+    }
+
+    override fun instantiateAdapter(): RecyclerView.Adapter<*> {
+        // Also remove animation
+        recyclerView?.itemAnimator = null
+        return controller.adapter
+    }
+
+    override fun onItemClick(t: RoomSummary) {
+        dispatchClick(t)
+    }
+
+    override fun onQuery(query: CharSequence?) {
+        val queryParams = roomSummaryQueryParams {
+            canonicalAlias = if (query.isNullOrBlank()) {
+                QueryStringValue.IsNotNull
+            } else {
+                QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+            }
+        }
+        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 5c2d2c49c0..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt
+++ /dev/null
@@ -1,54 +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 com.airbnb.epoxy.EpoxyController
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.Success
-import im.vector.matrix.android.api.session.user.model.User
-import im.vector.riotx.features.autocomplete.EpoxyAutocompletePresenter
-import javax.inject.Inject
-
-class AutocompleteUserPresenter @Inject constructor(context: Context,
-                                                    private val controller: AutocompleteUserController
-) : EpoxyAutocompletePresenter(context) {
-
-    var callback: Callback? = null
-
-    init {
-        controller.listener = this
-    }
-
-    override fun providesController(): EpoxyController {
-        return controller
-    }
-
-    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/grouplist/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt
index 71c38c1117..816f721040 100644
--- a/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt
@@ -25,7 +25,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
@@ -97,6 +99,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()
@@ -110,9 +116,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/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
index 9ec31d4422..ac4d29dd96 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
@@ -53,6 +53,14 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
                 DrawableImageViewTarget(imageView))
     }
 
+    @UiThread
+    fun render(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) {
+        render(imageView.context,
+                glideRequests,
+                matrixItem,
+                DrawableImageViewTarget(imageView))
+    }
+
     @UiThread
     fun render(context: Context,
                glideRequest: GlideRequests,
@@ -64,6 +72,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 fc0eeaf92c..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
@@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
 import im.vector.matrix.android.api.util.toMatrixItem
 import im.vector.riotx.R
 import im.vector.riotx.core.extensions.commitTransactionNow
+import im.vector.riotx.core.glide.GlideApp
 import im.vector.riotx.core.platform.ToolbarConfigurable
 import im.vector.riotx.core.platform.VectorBaseFragment
 import im.vector.riotx.core.ui.views.KeysBackupBanner
@@ -65,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())
         }
@@ -75,7 +81,8 @@ class HomeDetailFragment @Inject constructor(
 
     private fun onGroupChange(groupSummary: GroupSummary?) {
         groupSummary?.let {
-            avatarRenderer.render(it.toMatrixItem(), groupToolbarAvatarImageView)
+            // Use GlideApp with activity context to avoid the glideRequests to be paused
+            avatarRenderer.render(it.toMatrixItem(), groupToolbarAvatarImageView, GlideApp.with(requireActivity()))
         }
     }
 
@@ -125,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
@@ -147,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) {
@@ -192,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 64ac5c2995..9aa9313ad2 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/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt
new file mode 100644
index 0000000000..7ca647ea3e
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt
@@ -0,0 +1,229 @@
+/*
+ * 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
+
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.text.Editable
+import android.text.Spannable
+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.util.MatrixItem
+import im.vector.matrix.android.api.util.toMatrixItem
+import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
+import im.vector.riotx.R
+import im.vector.riotx.core.glide.GlideApp
+import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
+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.command.Command
+import im.vector.riotx.features.home.AvatarRenderer
+import im.vector.riotx.features.html.PillImageSpan
+import im.vector.riotx.features.themes.ThemeUtils
+
+class AutoCompleter @AssistedInject constructor(
+        @Assisted val roomId: String,
+        private val avatarRenderer: AvatarRenderer,
+        private val commandAutocompletePolicy: CommandAutocompletePolicy,
+        private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
+        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() {
+        commandAutocompletePolicy.enabled = false
+    }
+
+    fun exitSpecialMode() {
+        commandAutocompletePolicy.enabled = true
+    }
+
+    private val glideRequests by lazy {
+        GlideApp.with(editText)
+    }
+
+    fun setup(editText: EditText) {
+        this.editText = editText
+        val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background))
+        setupCommands(backgroundDrawable, editText)
+        setupMembers(backgroundDrawable, editText)
+        setupGroups(backgroundDrawable, editText)
+        setupEmojis(backgroundDrawable, editText)
+        setupRooms(backgroundDrawable, editText)
+    }
+
+    private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) {
+        Autocomplete.on(editText)
+                .with(commandAutocompletePolicy)
+                .with(autocompleteCommandPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
+                        editable.clear()
+                        editable
+                                .append(item.command)
+                                .append(" ")
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
+        val autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
+        Autocomplete.on(editText)
+                .with(CharPolicy('@', true))
+                .with(autocompleteMemberPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: RoomMember): Boolean {
+                        insertMatrixItem(editText, editable, "@", item.toMatrixItem())
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
+        Autocomplete.on(editText)
+                .with(CharPolicy('#', true))
+                .with(autocompleteRoomPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
+                        insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem())
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
+        Autocomplete.on(editText)
+                .with(CharPolicy('+', true))
+                .with(autocompleteGroupPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
+                        insertMatrixItem(editText, editable, "+", item.toMatrixItem())
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
+        Autocomplete.on(editText)
+                .with(CharPolicy(':', false))
+                .with(autocompleteEmojiPresenter)
+                .with(ELEVATION)
+                .with(backgroundDrawable)
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
+                        // Detect last ":" and remove it
+                        var startIndex = editable.lastIndexOf(":")
+                        if (startIndex == -1) {
+                            startIndex = 0
+                        }
+
+                        // Detect next word separator
+                        var endIndex = editable.indexOf(" ", startIndex)
+                        if (endIndex == -1) {
+                            endIndex = editable.length
+                        }
+
+                        // Replace the word by its completion
+                        editable.replace(startIndex, endIndex, item)
+                        return true
+                    }
+
+                    override fun onPopupVisibilityChanged(shown: Boolean) {
+                    }
+                })
+                .build()
+    }
+
+    private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) {
+        // Detect last firstChar and remove it
+        var startIndex = editable.lastIndexOf(firstChar)
+        if (startIndex == -1) {
+            startIndex = 0
+        }
+
+        // Detect next word separator
+        var endIndex = editable.indexOf(" ", startIndex)
+        if (endIndex == -1) {
+            endIndex = editable.length
+        }
+
+        // Replace the word by its completion
+        val displayName = matrixItem.getBestName()
+
+        // with a trailing space
+        editable.replace(startIndex, endIndex, "$displayName ")
+
+        // Add the span
+        val span = PillImageSpan(
+                glideRequests,
+                avatarRenderer,
+                editText.context,
+                matrixItem
+        )
+        span.bind(editText)
+
+        editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+    }
+
+    companion object {
+        private const val ELEVATION = 6f
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
new file mode 100644
index 0000000000..4be5502678
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.riotx.features.home.room.detail
+
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import im.vector.riotx.core.utils.Debouncer
+import timber.log.Timber
+
+/**
+ * Show or hide the jumpToBottomView, depending on the scrolling and if the timeline is displaying the more recent event
+ * - When user scrolls up (i.e. going to the past): hide
+ * - When user scrolls down: show if not displaying last event
+ * - When user stops scrolling: show if not displaying last event
+ */
+class JumpToBottomViewVisibilityManager(
+        private val jumpToBottomView: FloatingActionButton,
+        private val debouncer: Debouncer,
+        recyclerView: RecyclerView,
+        private val layoutManager: LinearLayoutManager) {
+
+    init {
+        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+                debouncer.cancel("jump_to_bottom_visibility")
+
+                val scrollingToPast = dy < 0
+
+                if (scrollingToPast) {
+                    jumpToBottomView.hide()
+                } else {
+                    maybeShowJumpToBottomViewVisibility()
+                }
+            }
+
+            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+                when (newState) {
+                    RecyclerView.SCROLL_STATE_IDLE     -> {
+                        maybeShowJumpToBottomViewVisibilityWithDelay()
+                    }
+                    RecyclerView.SCROLL_STATE_DRAGGING,
+                    RecyclerView.SCROLL_STATE_SETTLING -> Unit
+                }
+            }
+        })
+    }
+
+    fun maybeShowJumpToBottomViewVisibilityWithDelay() {
+        debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
+            maybeShowJumpToBottomViewVisibility()
+        })
+    }
+
+    private fun maybeShowJumpToBottomViewVisibility() {
+        Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
+        if (layoutManager.findFirstVisibleItemPosition() != 0) {
+            jumpToBottomView.show()
+        } else {
+            jumpToBottomView.hide()
+        }
+    }
+}
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 46f46657eb..b5560129ae 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
@@ -20,12 +20,10 @@ import android.annotation.SuppressLint
 import android.app.Activity.RESULT_OK
 import android.content.DialogInterface
 import android.content.Intent
-import android.graphics.drawable.ColorDrawable
 import android.net.Uri
 import android.os.Build
 import android.os.Bundle
 import android.os.Parcelable
-import android.text.Editable
 import android.text.Spannable
 import android.view.*
 import android.widget.TextView
@@ -52,9 +50,6 @@ import com.github.piasy.biv.BigImageViewer
 import com.github.piasy.biv.loader.ImageLoader
 import com.google.android.material.snackbar.Snackbar
 import com.google.android.material.textfield.TextInputEditText
-import com.otaliastudios.autocomplete.Autocomplete
-import com.otaliastudios.autocomplete.AutocompleteCallback
-import com.otaliastudios.autocomplete.CharPolicy
 import im.vector.matrix.android.api.permalinks.PermalinkFactory
 import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.session.content.ContentAttachmentData
@@ -65,7 +60,6 @@ import im.vector.matrix.android.api.session.room.send.SendState
 import im.vector.matrix.android.api.session.room.timeline.Timeline
 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
-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.riotx.R
@@ -81,17 +75,9 @@ import im.vector.riotx.core.utils.*
 import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
 import im.vector.riotx.features.attachments.AttachmentsHelper
 import im.vector.riotx.features.attachments.ContactAttachment
-import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
-import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
-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.core.utils.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
@@ -113,7 +99,6 @@ import im.vector.riotx.features.permalink.PermalinkHandler
 import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
 import im.vector.riotx.features.settings.VectorPreferences
 import im.vector.riotx.features.share.SharedData
-import im.vector.riotx.features.themes.ThemeUtils
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.schedulers.Schedulers
 import kotlinx.android.parcel.Parcelize
@@ -138,19 +123,15 @@ class RoomDetailFragment @Inject constructor(
         private val session: Session,
         private val avatarRenderer: AvatarRenderer,
         private val timelineEventController: TimelineEventController,
-        private val commandAutocompletePolicy: CommandAutocompletePolicy,
-        private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
-        private val autocompleteUserPresenter: AutocompleteUserPresenter,
+        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,
-        AutocompleteUserPresenter.Callback,
         VectorInviteView.Callback,
         JumpToReadMarkerView.Callback,
         AttachmentTypeSelectorView.Callback,
@@ -180,9 +161,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
@@ -194,6 +176,7 @@ class RoomDetailFragment @Inject constructor(
 
     private lateinit var sharedActionViewModel: MessageSharedActionViewModel
     private lateinit var layoutManager: LinearLayoutManager
+    private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager
     private var modelBuildListener: OnModelBuildFinishedListener? = null
 
     private lateinit var attachmentsHelper: AttachmentsHelper
@@ -221,8 +204,7 @@ class RoomDetailFragment @Inject constructor(
             navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
         }
         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())
@@ -265,9 +247,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)
             }
         }
 
@@ -278,15 +260,25 @@ class RoomDetailFragment @Inject constructor(
         roomDetailViewModel.requestLiveData.observeEvent(this) {
             displayRoomDetailActionResult(it)
         }
+
+        roomDetailViewModel.viewEvents
+                .observe()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe {
+                    when (it) {
+                        is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
+                    }
+                }
+                .disposeOnDestroyView()
     }
 
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         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")
             }
         }
     }
@@ -310,14 +302,19 @@ 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)
             }
         }
+
+        jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager(
+                jumpToBottomView,
+                debouncer,
+                recyclerView,
+                layoutManager
+        )
     }
 
     private fun setupJumpToReadMarkerView() {
@@ -382,7 +379,7 @@ class RoomDetailFragment @Inject constructor(
     }
 
     private fun renderRegularMode(text: String) {
-        commandAutocompletePolicy.enabled = true
+        autoCompleter.exitSpecialMode()
         composerLayout.collapse()
 
         updateComposerText(text)
@@ -393,7 +390,7 @@ class RoomDetailFragment @Inject constructor(
                                   @DrawableRes iconRes: Int,
                                   @StringRes descriptionRes: Int,
                                   defaultContent: String) {
-        commandAutocompletePolicy.enabled = false
+        autoCompleter.enterSpecialMode()
         // switch to expanded bar
         composerLayout.composerRelatedMessageTitle.apply {
             text = event.getDisambiguatedDisplayName()
@@ -417,7 +414,8 @@ class RoomDetailFragment @Inject constructor(
 
 
         avatarRenderer.render(
-                MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
+                MatrixItem.UserItem(event.root.senderId
+                        ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
                 composerLayout.composerRelatedMessageAvatar
         )
 
@@ -468,6 +466,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)
@@ -482,27 +483,11 @@ class RoomDetailFragment @Inject constructor(
             it.dispatchTo(scrollOnNewMessageCallback)
             it.dispatchTo(scrollOnHighlightedEventCallback)
             updateJumpToReadMarkerViewVisibility()
-            updateJumpToBottomViewVisibility()
+            jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
         }
         timelineEventController.addModelBuildListener(modelBuildListener)
         recyclerView.adapter = timelineEventController.adapter
 
-        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
-            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
-                when (newState) {
-                    RecyclerView.SCROLL_STATE_IDLE     -> {
-                        updateJumpToBottomViewVisibility()
-                    }
-                    RecyclerView.SCROLL_STATE_DRAGGING,
-                    RecyclerView.SCROLL_STATE_SETTLING -> {
-                        jumpToBottomView.hide()
-                    }
-                }
-            }
-        })
-
-        timelineEventController.callback = this
-
         if (vectorPreferences.swipeToReplyIsEnabled()) {
             val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {
                 override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
@@ -519,7 +504,7 @@ class RoomDetailFragment @Inject constructor(
                         is MessageTextItem -> {
                             return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
                         }
-                        else               -> false
+                        else -> false
                     }
                 }
             }
@@ -534,9 +519,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()
@@ -550,90 +535,13 @@ class RoomDetailFragment @Inject constructor(
                         }
                     }
                 }
-                jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
+                jumpToReadMarkerView?.isVisible = showJumpToUnreadBanner
             }
         }
     }
 
-    private fun updateJumpToBottomViewVisibility() {
-        debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
-            Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
-            if (layoutManager.findFirstVisibleItemPosition() != 0) {
-                jumpToBottomView.show()
-            } else {
-                jumpToBottomView.hide()
-            }
-        })
-    }
-
     private fun setupComposer() {
-        val elevation = 6f
-        val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background))
-        Autocomplete.on(composerLayout.composerEditText)
-                .with(commandAutocompletePolicy)
-                .with(autocompleteCommandPresenter)
-                .with(elevation)
-                .with(backgroundDrawable)
-                .with(object : AutocompleteCallback {
-                    override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
-                        editable.clear()
-                        editable
-                                .append(item.command)
-                                .append(" ")
-                        return true
-                    }
-
-                    override fun onPopupVisibilityChanged(shown: Boolean) {
-                    }
-                })
-                .build()
-
-        autocompleteUserPresenter.callback = this
-        Autocomplete.on(composerLayout.composerEditText)
-                .with(CharPolicy('@', true))
-                .with(autocompleteUserPresenter)
-                .with(elevation)
-                .with(backgroundDrawable)
-                .with(object : AutocompleteCallback {
-                    override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
-                        // Detect last '@' and remove it
-                        var startIndex = editable.lastIndexOf("@")
-                        if (startIndex == -1) {
-                            startIndex = 0
-                        }
-
-                        // Detect next word separator
-                        var endIndex = editable.indexOf(" ", startIndex)
-                        if (endIndex == -1) {
-                            endIndex = editable.length
-                        }
-
-                        // Replace the word by its completion
-                        val matrixItem = item.toMatrixItem()
-                        val displayName = matrixItem.getBestName()
-
-                        // with a trailing space
-                        editable.replace(startIndex, endIndex, "$displayName ")
-
-                        // Add the span
-                        val span = PillImageSpan(
-                                glideRequests,
-                                avatarRenderer,
-                                requireContext(),
-                                matrixItem
-                        )
-                        span.bind(composerLayout.composerEditText)
-
-                        editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-
-                        return true
-                    }
-
-                    override fun onPopupVisibilityChanged(shown: Boolean) {
-                    }
-                })
-                .build()
-
+        autoCompleter.setup(composerLayout.composerEditText)
         composerLayout.callback = object : TextComposerView.Callback {
             override fun onAddAttachment() {
                 if (!::attachmentTypeSelector.isInitialized) {
@@ -688,7 +596,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
@@ -703,9 +611,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
@@ -728,10 +637,6 @@ class RoomDetailFragment @Inject constructor(
         }
     }
 
-    private fun renderTextComposerState(state: TextComposerViewState) {
-        autocompleteUserPresenter.render(state.asyncUsers)
-    }
-
     private fun renderTombstoneEventHandling(async: Async) {
         when (async) {
             is Loading -> {
@@ -744,7 +649,7 @@ class RoomDetailFragment @Inject constructor(
                 navigator.openRoom(vectorBaseActivity, async())
                 vectorBaseActivity.finish()
             }
-            is Fail    -> {
+            is Fail -> {
                 vectorBaseActivity.hideWaitingView()
                 vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error))
             }
@@ -753,23 +658,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 -> {
@@ -807,7 +712,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))
@@ -818,7 +723,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)
@@ -840,7 +745,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)
@@ -953,14 +858,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
@@ -1056,31 +961,25 @@ class RoomDetailFragment @Inject constructor(
         roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
     }
 
-    // AutocompleteUserPresenter.Callback
-
-    override fun onQueryUsers(query: CharSequence?) {
-        textComposerViewModel.handle(TextComposerAction.QueryUsers(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
@@ -1108,10 +1007,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
@@ -1122,7 +1021,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
@@ -1133,31 +1032,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))
             }
@@ -1165,19 +1064,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()
             }
         }
@@ -1284,10 +1183,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/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt
similarity index 70%
rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt
rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt
index 5d60fa1cef..a1ad480584 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package im.vector.riotx.features.home.room.detail.composer
+package im.vector.riotx.features.home.room.detail
 
-import im.vector.riotx.core.platform.VectorViewModelAction
-
-sealed class TextComposerAction : VectorViewModelAction {
-    data class QueryUsers(val query: CharSequence?) : TextComposerAction()
+/**
+ * Transient events for RoomDetail
+ */
+sealed class RoomDetailViewEvents {
+    data class Failure(val throwable: Throwable) : RoomDetailViewEvents()
 }
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 e7a18753cd..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,7 +20,12 @@ 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
@@ -56,7 +61,9 @@ import im.vector.riotx.core.extensions.postLiveEvent
 import im.vector.riotx.core.platform.VectorViewModel
 import im.vector.riotx.core.resources.StringProvider
 import im.vector.riotx.core.resources.UserPreferencesProvider
+import im.vector.riotx.core.utils.DataSource
 import im.vector.riotx.core.utils.LiveEvent
+import im.vector.riotx.core.utils.PublishDataSource
 import im.vector.riotx.core.utils.subscribeLogError
 import im.vector.riotx.features.command.CommandParser
 import im.vector.riotx.features.command.ParsedCommand
@@ -86,20 +93,24 @@ 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
 
     // Can be used for several actions, for a one shot result
     private val _requestLiveData = MutableLiveData>>()
@@ -132,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)
     }
@@ -227,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("")
                         )
                     }
                 }
@@ -252,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
@@ -304,7 +314,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         else                     -> false
     }
 
-    // PRIVATE METHODS *****************************************************************************
+// PRIVATE METHODS *****************************************************************************
 
     private fun handleSendMessage(action: RoomDetailAction.SendMessage) {
         withState { state ->
@@ -390,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 {
@@ -399,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")
                         }
@@ -416,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())
@@ -532,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)))
             }
         }
     }
@@ -722,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) {
@@ -785,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)
                 }
     }
 
@@ -819,9 +826,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         if (events.isEmpty()) return UnreadState.Unknown
         val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
         val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot)
-                ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
         val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId)
-                ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
+        if (firstDisplayableEventId == null || firstDisplayableEventIndex == null) {
+            return if (timeline.isLive) {
+                UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
+            } else {
+                UnreadState.Unknown
+            }
+        }
         for (i in (firstDisplayableEventIndex - 1) downTo 0) {
             val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
             val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown
@@ -857,13 +869,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     }
 
-    override fun onUpdated(snapshot: List) {
+    override fun onTimelineUpdated(snapshot: List) {
         timelineEvents.accept(snapshot)
     }
 
+    override fun onTimelineFailure(throwable: Throwable) {
+        // If we have a critical timeline issue, we get back to live.
+        timeline.restartWithEventId(null)
+        _viewEvents.post(RoomDetailViewEvents.Failure(throwable))
+    }
+
     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 88548e12b4..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt
+++ /dev/null
@@ -1,97 +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.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 AutocompleteUserQuery = CharSequence
-
-class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState,
-                                                        private val session: Session
-) : VectorViewModel(initialState) {
-
-    private val room = session.getRoom(initialState.roomId)!!
-    private val roomId = initialState.roomId
-
-    private val usersQueryObservable = 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()
-    }
-
-    override fun handle(action: TextComposerAction) {
-        when (action) {
-            is TextComposerAction.QueryUsers -> handleQueryUsers(action)
-        }
-    }
-
-    private fun handleQueryUsers(action: TextComposerAction.QueryUsers) {
-        val query = Option.fromNullable(action.query)
-        usersQueryObservable.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?.startsWith(prefix = filter, ignoreCase = true) ?: false
-                        }
-                    }
-                }
-        ).execute { async ->
-            copy(
-                    asyncUsers = async
-            )
-        }
-    }
-}
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 576b9fa0ba..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
     }
 
@@ -220,7 +217,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
         showingForwardLoader = LoadingItem_()
                 .id("forward_loading_item_$timestamp")
                 .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
-                .addWhen(Timeline.Direction.FORWARDS)
+                .addWhenLoading(Timeline.Direction.FORWARDS)
 
         val timelineModels = getModels()
         add(timelineModels)
@@ -230,16 +227,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             LoadingItem_()
                     .id("backward_loading_item_$timestamp")
                     .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
-                    .addWhen(Timeline.Direction.BACKWARDS)
+                    .addWhenLoading(Timeline.Direction.BACKWARDS)
         }
     }
 
 // Timeline.LISTENER ***************************************************************************
 
-    override fun onUpdated(snapshot: List) {
+    override fun onTimelineUpdated(snapshot: List) {
         submitSnapshot(snapshot)
     }
 
+    override fun onTimelineFailure(throwable: Throwable) {
+        // no-op, already handled
+    }
+
     private fun submitSnapshot(newSnapshot: List) {
         backgroundHandler.post {
             inSubmitList = true
@@ -247,6 +248,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             currentSnapshot = newSnapshot
             val diffResult = DiffUtil.calculateDiff(diffCallback)
             diffResult.dispatchUpdatesTo(listUpdateCallback)
+            requestDelayedModelBuild(100)
             inSubmitList = false
         }
     }
@@ -319,7 +321,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
     /**
      * Return true if added
      */
-    private fun LoadingItem_.addWhen(direction: Timeline.Direction): Boolean {
+    private fun LoadingItem_.addWhenLoading(direction: Timeline.Direction): Boolean {
         val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false
         addIf(shouldAdd, this@TimelineEventController)
         return shouldAdd
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 f72659dce4..ac77e5de3d 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
@@ -94,7 +94,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
         }
 
         // Action
-        state.actions()?.forEachIndexed { index, action ->
+        state.actions.forEachIndexed { index, action ->
             if (action is EventSharedAction.Separator) {
                 dividerItem {
                     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 1303c3aad9..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,6 +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.reactions.data.EmojiDataSource
+import im.vector.riotx.features.settings.VectorPreferences
 import java.text.SimpleDateFormat
 import java.util.*
 
@@ -62,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 {
 
@@ -86,7 +87,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                                                           private val htmlCompressor: VectorHtmlCompressor,
                                                           private val session: Session,
                                                           private val noticeEventFormatter: NoticeEventFormatter,
-                                                          private val stringProvider: StringProvider
+                                                          private val stringProvider: StringProvider,
+                                                          private val vectorPreferences: VectorPreferences
 ) : VectorViewModel(initialState) {
 
     private val eventId = initialState.eventId
@@ -99,9 +101,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     }
 
     companion object : MvRxViewModelFactory {
-
-        val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
-
         @JvmStatic
         override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
             val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
@@ -112,7 +111,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     init {
         observeEvent()
         observeReactions()
-        observeEventAction()
+        observeTimelineEventState()
     }
 
     override fun handle(action: MessageActionsAction) {
@@ -131,35 +130,20 @@ 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 ->
-                    quickEmojis.map { emoji ->
+                    EmojiDataSource.quickEmojis.map { emoji ->
                         ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
                     }
                 }
@@ -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() }
@@ -187,45 +179,45 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
             EventType.STATE_ROOM_NAME,
             EventType.STATE_ROOM_TOPIC,
             EventType.STATE_ROOM_MEMBER,
-            EventType.STATE_HISTORY_VISIBILITY,
+            EventType.STATE_ROOM_ALIASES,
+            EventType.STATE_ROOM_CANONICAL_ALIAS,
+            EventType.STATE_ROOM_HISTORY_VISIBILITY,
             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))
                     }
 
@@ -234,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))
                     }
 
@@ -259,29 +251,30 @@ 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
                     }
                 }
 
-                add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent()))
-                if (event.isEncrypted()) {
-                    val decryptedContent = event.root.toClearContentStringWithIndent()
-                            ?: stringProvider.getString(R.string.encryption_information_decryption_error)
-                    add(EventSharedAction.ViewDecryptedSource(decryptedContent))
+                if (vectorPreferences.developerMode()) {
+                    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/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 5b6dec9900..4a7a1e2a86 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -45,8 +45,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                 EventType.STATE_ROOM_NAME,
                 EventType.STATE_ROOM_TOPIC,
                 EventType.STATE_ROOM_MEMBER,
+                EventType.STATE_ROOM_ALIASES,
+                EventType.STATE_ROOM_CANONICAL_ALIAS,
                 EventType.STATE_ROOM_JOIN_RULES,
-                EventType.STATE_HISTORY_VISIBILITY,
+                EventType.STATE_ROOM_HISTORY_VISIBILITY,
                 EventType.CALL_INVITE,
                 EventType.CALL_HANGUP,
                 EventType.CALL_ANSWER,
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 75100e6c03..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
@@ -33,19 +33,21 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
 
     fun format(timelineEvent: TimelineEvent): CharSequence? {
         return when (val type = timelineEvent.root.getClearType()) {
-            EventType.STATE_ROOM_JOIN_RULES    -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
-            EventType.STATE_ROOM_NAME          -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
-            EventType.STATE_ROOM_TOPIC         -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
-            EventType.STATE_ROOM_MEMBER        -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
-            EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
-            EventType.STATE_ROOM_TOMBSTONE     -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName())
+            EventType.STATE_ROOM_JOIN_RULES         -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
+            EventType.STATE_ROOM_NAME               -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
+            EventType.STATE_ROOM_TOPIC              -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
+            EventType.STATE_ROOM_MEMBER             -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
+            EventType.STATE_ROOM_ALIASES            -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
+            EventType.STATE_ROOM_CANONICAL_ALIAS    -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
+            EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
+            EventType.STATE_ROOM_TOMBSTONE          -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName())
             EventType.CALL_INVITE,
             EventType.CALL_HANGUP,
-            EventType.CALL_ANSWER              -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
+            EventType.CALL_ANSWER                   -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
             EventType.MESSAGE,
             EventType.REACTION,
-            EventType.REDACTION                -> formatDebug(timelineEvent.root)
-            else                               -> {
+            EventType.REDACTION                     -> formatDebug(timelineEvent.root)
+            else                                    -> {
                 Timber.v("Type $type not handled by this formatter")
                 null
             }
@@ -54,16 +56,16 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
 
     fun format(event: Event, senderName: String?): CharSequence? {
         return when (val type = event.getClearType()) {
-            EventType.STATE_ROOM_JOIN_RULES    -> formatJoinRulesEvent(event, senderName)
-            EventType.STATE_ROOM_NAME          -> formatRoomNameEvent(event, senderName)
-            EventType.STATE_ROOM_TOPIC         -> formatRoomTopicEvent(event, senderName)
-            EventType.STATE_ROOM_MEMBER        -> formatRoomMemberEvent(event, senderName)
-            EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
+            EventType.STATE_ROOM_JOIN_RULES         -> formatJoinRulesEvent(event, senderName)
+            EventType.STATE_ROOM_NAME               -> formatRoomNameEvent(event, senderName)
+            EventType.STATE_ROOM_TOPIC              -> formatRoomTopicEvent(event, senderName)
+            EventType.STATE_ROOM_MEMBER             -> formatRoomMemberEvent(event, senderName)
+            EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
             EventType.CALL_INVITE,
             EventType.CALL_HANGUP,
-            EventType.CALL_ANSWER              -> formatCallEvent(event, senderName)
-            EventType.STATE_ROOM_TOMBSTONE     -> formatRoomTombstoneEvent(senderName)
-            else                               -> {
+            EventType.CALL_ANSWER                   -> formatCallEvent(event, senderName)
+            EventType.STATE_ROOM_TOMBSTONE          -> formatRoomTombstoneEvent(senderName)
+            else                                    -> {
                 Timber.v("Type $type not handled by this formatter")
                 null
             }
@@ -126,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)
@@ -136,7 +138,35 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
         }
     }
 
-    private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String {
+    private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? {
+        val eventContent: RoomAliasesContent? = event.getClearContent().toModel()
+        val prevEventContent: RoomAliasesContent? = event.unsignedData?.prevContent?.toModel()
+
+        val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty()
+        val removedAliases = prevEventContent?.aliases.orEmpty() - eventContent?.aliases.orEmpty()
+
+        return if (addedAliases.isNotEmpty() && removedAliases.isNotEmpty()) {
+            sp.getString(R.string.notice_room_aliases_added_and_removed, senderName, addedAliases.joinToString(), removedAliases.joinToString())
+        } else if (addedAliases.isNotEmpty()) {
+            sp.getQuantityString(R.plurals.notice_room_aliases_added, addedAliases.size, senderName, addedAliases.joinToString())
+        } else if (removedAliases.isNotEmpty()) {
+            sp.getQuantityString(R.plurals.notice_room_aliases_removed, removedAliases.size, senderName, removedAliases.joinToString())
+        } else {
+            Timber.w("Alias event without any change...")
+            null
+        }
+    }
+
+    private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? {
+        val eventContent: RoomCanonicalAliasContent? = event.getClearContent().toModel()
+        val canonicalAlias = eventContent?.canonicalAlias
+        return canonicalAlias
+                ?.takeIf { it.isNotBlank() }
+                ?.let { sp.getString(R.string.notice_room_canonical_alias_set, senderName, it) }
+                ?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName)
+    }
+
+    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) {
@@ -168,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/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index d0098201bd..0a8f1e11bb 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -33,6 +33,7 @@ import me.gujun.android.span.span
 import javax.inject.Inject
 
 /**
+ * TODO Update this comment
  * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
  */
 class MessageInformationDataFactory @Inject constructor(private val session: Session,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
index 1cd851f8c8..b0f3e617a6 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
@@ -27,7 +27,9 @@ object TimelineDisplayableEvents {
             EventType.STATE_ROOM_NAME,
             EventType.STATE_ROOM_TOPIC,
             EventType.STATE_ROOM_MEMBER,
-            EventType.STATE_HISTORY_VISIBILITY,
+            EventType.STATE_ROOM_ALIASES,
+            EventType.STATE_ROOM_CANONICAL_ALIAS,
+            EventType.STATE_ROOM_HISTORY_VISIBILITY,
             EventType.CALL_INVITE,
             EventType.CALL_HANGUP,
             EventType.CALL_ANSWER,
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 bc13e13e96..4d9f5fb847 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
@@ -60,7 +60,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 {
 
@@ -96,7 +97,6 @@ class RoomListFragment @Inject constructor(
         setupCreateRoomButton()
         setupRecyclerView()
         sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
-
         roomListViewModel.subscribe { renderState(it) }
         roomListViewModel.viewEvents
                 .observe()
@@ -104,7 +104,7 @@ class RoomListFragment @Inject constructor(
                 .subscribe {
                     when (it) {
                         is RoomListViewEvents.SelectRoom -> openSelectedRoom(it)
-                        is RoomListViewEvents.Failure    -> showError(it)
+                        is RoomListViewEvents.Failure    -> showErrorInSnackbar(it.throwable)
                     }
                 }
                 .disposeOnDestroyView()
@@ -135,10 +135,6 @@ class RoomListFragment @Inject constructor(
         }
     }
 
-    private fun showError(event: RoomListViewEvents.Failure) {
-        vectorBaseActivity.showSnackbar(errorFormatter.toHumanReadable(event.throwable))
-    }
-
     private fun setupCreateRoomButton() {
         when (roomListParams.displayMode) {
             RoomListDisplayMode.HOME   -> createChatFabMenu.isVisible = true
@@ -198,6 +194,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 ae4d347602..1e775a934a 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
@@ -53,6 +53,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
@@ -77,7 +78,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/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt
index 3f16666221..76f7dfaabd 100644
--- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt
+++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt
@@ -20,6 +20,7 @@ import android.content.Context
 import android.text.style.URLSpan
 import im.vector.matrix.android.api.permalinks.PermalinkData
 import im.vector.matrix.android.api.permalinks.PermalinkParser
+import im.vector.matrix.android.api.session.room.model.RoomSummary
 import im.vector.matrix.android.api.util.MatrixItem
 import im.vector.riotx.core.di.ActiveSessionHolder
 import im.vector.riotx.core.glide.GlideRequests
@@ -39,26 +40,47 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests,
         val link = tag.attributes()["href"]
         if (link != null) {
             val permalinkData = PermalinkParser.parse(link)
-            when (permalinkData) {
-                is PermalinkData.UserLink -> {
+            val matrixItem = when (permalinkData) {
+                is PermalinkData.UserLink  -> {
                     val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
-                    val span = PillImageSpan(glideRequests, avatarRenderer, context, MatrixItem.UserItem(permalinkData.userId, user?.displayName
-                            ?: permalinkData.userId, user?.avatarUrl))
-                    SpannableBuilder.setSpans(
-                            visitor.builder(),
-                            span,
-                            tag.start(),
-                            tag.end()
-                    )
-                    // also add clickable span
-                    SpannableBuilder.setSpans(
-                            visitor.builder(),
-                            URLSpan(link),
-                            tag.start(),
-                            tag.end()
-                    )
+                    MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl)
                 }
-                else                      -> super.handle(visitor, renderer, tag)
+                is PermalinkData.RoomLink  -> {
+                    if (permalinkData.eventId == null) {
+                        val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
+                        if (permalinkData.isRoomAlias) {
+                            MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
+                        } else {
+                            MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
+                        }
+                    } else {
+                        // Exclude event link (used in reply events, we do not want to pill the "in reply to")
+                        null
+                    }
+                }
+                is PermalinkData.GroupLink -> {
+                    val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
+                    MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
+                }
+                else                       -> null
+            }
+
+            if (matrixItem == null) {
+                super.handle(visitor, renderer, tag)
+            } else {
+                val span = PillImageSpan(glideRequests, avatarRenderer, context, matrixItem)
+                SpannableBuilder.setSpans(
+                        visitor.builder(),
+                        span,
+                        tag.start(),
+                        tag.end()
+                )
+                SpannableBuilder.setSpans(
+                        visitor.builder(),
+                        URLSpan(link),
+                        tag.start(),
+                        tag.end()
+                )
             }
         } else {
             super.handle(visitor, renderer, tag)
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 8b57006439..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
@@ -28,7 +28,7 @@ import androidx.annotation.UiThread
 import com.bumptech.glide.request.target.SimpleTarget
 import com.bumptech.glide.request.transition.Transition
 import com.google.android.material.chip.ChipDrawable
-import im.vector.matrix.android.api.session.room.send.UserMentionSpan
+import im.vector.matrix.android.api.session.room.send.MatrixItemSpan
 import im.vector.matrix.android.api.util.MatrixItem
 import im.vector.riotx.R
 import im.vector.riotx.core.glide.GlideRequests
@@ -38,13 +38,13 @@ import java.lang.ref.WeakReference
 /**
  * This span is able to replace a text by a [ChipDrawable]
  * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
- * Implements UserMentionSpan so that it could be automatically transformed in matrix links and displayed as pills.
+ * Implements MatrixItemSpan so that it could be automatically transformed in matrix links and displayed as pills.
  */
 class PillImageSpan(private val glideRequests: GlideRequests,
                     private val avatarRenderer: AvatarRenderer,
                     private val context: Context,
                     override val matrixItem: MatrixItem
-) : ReplacementSpan(), UserMentionSpan {
+) : ReplacementSpan(), MatrixItemSpan {
 
     private val pillDrawable = createChipDrawable()
     private val target = PillImageSpanTarget(this)
@@ -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/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt
index 64fb01fa5f..3ee1cd6d64 100644
--- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt
@@ -30,8 +30,8 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil
 import com.jakewharton.rxbinding3.widget.textChanges
 import im.vector.matrix.android.api.auth.registration.RegisterThreePid
 import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.failure.is401
 import im.vector.riotx.R
-import im.vector.riotx.core.error.is401
 import im.vector.riotx.core.extensions.hideKeyboard
 import im.vector.riotx.core.extensions.isEmail
 import im.vector.riotx.core.extensions.setTextOrHide
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt
index e7ddc78853..cace48b7f2 100644
--- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt
@@ -20,8 +20,8 @@ import androidx.appcompat.app.AlertDialog
 import butterknife.OnClick
 import com.airbnb.mvrx.Fail
 import com.airbnb.mvrx.Success
+import im.vector.matrix.android.api.failure.is401
 import im.vector.riotx.R
-import im.vector.riotx.core.error.is401
 import kotlinx.android.synthetic.main.fragment_login_reset_password_mail_confirmation.*
 import javax.inject.Inject
 
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
index baa4160351..8eb4652da5 100644
--- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
@@ -16,8 +16,15 @@
 
 package im.vector.riotx.features.login
 
+import android.content.Context
 import androidx.fragment.app.FragmentActivity
-import com.airbnb.mvrx.*
+import com.airbnb.mvrx.ActivityViewModelContext
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.ViewModelContext
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import im.vector.matrix.android.api.MatrixCallback
@@ -46,6 +53,7 @@ import java.util.concurrent.CancellationException
  *
  */
 class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState,
+                                                 private val applicationContext: Context,
                                                  private val authenticationService: AuthenticationService,
                                                  private val activeSessionHolder: ActiveSessionHolder,
                                                  private val pushRuleTriggerListener: PushRuleTriggerListener,
@@ -486,7 +494,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
 
     private fun onSessionCreated(session: Session) {
         activeSessionHolder.setActiveSession(session)
-        session.configureAndStart(pushRuleTriggerListener, sessionListener)
+        session.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
         setState {
             copy(
                     asyncLoginAction = Success(Unit)
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt
index 8a12c67106..c4507876d7 100644
--- a/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt
@@ -20,8 +20,8 @@ import android.os.Bundle
 import android.os.Parcelable
 import android.view.View
 import com.airbnb.mvrx.args
+import im.vector.matrix.android.api.failure.is401
 import im.vector.riotx.R
-import im.vector.riotx.core.error.is401
 import kotlinx.android.parcel.Parcelize
 import kotlinx.android.synthetic.main.fragment_login_wait_for_email.*
 import javax.inject.Inject
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
index 5ec676d37e..8f7505928a 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
@@ -121,8 +121,8 @@ class DefaultNavigator @Inject constructor(
         context.startActivity(intent)
     }
 
-    override fun openSettings(context: Context) {
-        val intent = VectorSettingsActivity.getIntent(context)
+    override fun openSettings(context: Context, directAccess: Int) {
+        val intent = VectorSettingsActivity.getIntent(context, directAccess)
         context.startActivity(intent)
     }
 
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
index 6253da1c88..10eff8b2b3 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
@@ -19,6 +19,7 @@ package im.vector.riotx.features.navigation
 import android.app.Activity
 import android.content.Context
 import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
+import im.vector.riotx.features.settings.VectorSettingsActivity
 import im.vector.riotx.features.share.SharedData
 
 interface Navigator {
@@ -39,7 +40,7 @@ interface Navigator {
 
     fun openRoomsFiltering(context: Context)
 
-    fun openSettings(context: Context)
+    fun openSettings(context: Context, directAccess: Int = VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ROOT)
 
     fun openDebug(context: Context)
 
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/rageshake/RageShake.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt
index 39749be8c2..effed19c59 100644
--- a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt
+++ b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt
@@ -19,33 +19,32 @@ package im.vector.riotx.features.rageshake
 import android.content.Context
 import android.hardware.Sensor
 import android.hardware.SensorManager
-import android.preference.PreferenceManager
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.edit
 import com.squareup.seismic.ShakeDetector
 import im.vector.riotx.R
+import im.vector.riotx.core.hardware.vibrate
+import im.vector.riotx.features.navigation.Navigator
+import im.vector.riotx.features.settings.VectorPreferences
+import im.vector.riotx.features.settings.VectorSettingsActivity
 import javax.inject.Inject
 
 class RageShake @Inject constructor(private val activity: AppCompatActivity,
-                                    private val bugReporter: BugReporter) : ShakeDetector.Listener {
+                                    private val bugReporter: BugReporter,
+                                    private val navigator: Navigator,
+                                    private val vectorPreferences: VectorPreferences) : ShakeDetector.Listener {
 
     private var shakeDetector: ShakeDetector? = null
 
     private var dialogDisplayed = false
 
+    var interceptor: (() -> Unit)? = null
+
     fun start() {
-        if (!isEnable(activity)) {
-            return
-        }
-
-        val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager
-
-        if (sensorManager == null) {
-            return
-        }
+        val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager ?: return
 
         shakeDetector = ShakeDetector(this).apply {
+            setSensitivity(vectorPreferences.getRageshakeSensitivity())
             start(sensorManager)
         }
     }
@@ -54,52 +53,43 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity,
         shakeDetector?.stop()
     }
 
-    /**
-     * Enable the feature, and start it
-     */
-    fun enable() {
-        PreferenceManager.getDefaultSharedPreferences(activity).edit {
-            putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
-        }
-
-        start()
-    }
-
-    /**
-     * Disable the feature, and stop it
-     */
-    fun disable() {
-        PreferenceManager.getDefaultSharedPreferences(activity).edit {
-            putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, false)
-        }
-
-        stop()
+    fun setSensitivity(sensitivity: Int) {
+        shakeDetector?.setSensitivity(sensitivity)
     }
 
     override fun hearShake() {
-        if (dialogDisplayed) {
-            // Filtered!
-            return
+        val i = interceptor
+        if (i != null) {
+            vibrate(activity)
+            i.invoke()
+        } else {
+            if (dialogDisplayed) {
+                // Filtered!
+                return
+            }
+
+            vibrate(activity)
+            dialogDisplayed = true
+
+            AlertDialog.Builder(activity)
+                    .setMessage(R.string.send_bug_report_alert_message)
+                    .setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() }
+                    .setNeutralButton(R.string.settings) { _, _ -> openSettings() }
+                    .setOnDismissListener { dialogDisplayed = false }
+                    .setNegativeButton(R.string.no, null)
+                    .show()
         }
-
-        dialogDisplayed = true
-
-        AlertDialog.Builder(activity)
-                .setMessage(R.string.send_bug_report_alert_message)
-                .setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() }
-                .setNeutralButton(R.string.disable) { _, _ -> disable() }
-                .setOnDismissListener { dialogDisplayed = false }
-                .setNegativeButton(R.string.no, null)
-                .show()
     }
 
     private fun openBugReportScreen() {
         bugReporter.openBugReportScreen(activity)
     }
 
-    companion object {
-        private const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
+    private fun openSettings() {
+        navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS)
+    }
 
+    companion object {
         /**
          * Check if the feature is available
          */
@@ -107,12 +97,5 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity,
             return (context.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager)
                     ?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null
         }
-
-        /**
-         * Check if the feature is enable (enabled by default)
-         */
-        private fun isEnable(context: Context): Boolean {
-            return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
-        }
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt
index 01debac5ed..8aa03d9b22 100644
--- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt
@@ -56,26 +56,10 @@ class EmojiSearchResultViewModel @AssistedInject constructor(
     }
 
     private fun updateQuery(action: EmojiSearchAction.UpdateQuery) {
-        val words = action.queryString.split("\\s".toRegex())
         setState {
             copy(
                     query = action.queryString,
-                    // First add emojis with name matching query, sorted by name
-                    // Then emojis with keyword matching any of the word in the query, sorted by name
-                    results = dataSource.rawData.emojis
-                            .values
-                            .filter { emojiItem ->
-                                emojiItem.name.contains(action.queryString, true)
-                            }
-                            .sortedBy { it.name }
-                            + dataSource.rawData.emojis
-                            .values
-                            .filter { emojiItem ->
-                                words.fold(true, { prev, word ->
-                                    prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
-                                })
-                            }
-                            .sortedBy { it.name }
+                    results = dataSource.filterWith(action.queryString)
             )
         }
     }
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 a326828112..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
 ) {
@@ -33,4 +33,51 @@ class EmojiDataSource @Inject constructor(
                         .fromJson(input.bufferedReader().use { it.readText() })
             }
             ?: EmojiData(emptyList(), emptyMap(), emptyMap())
+
+    private val quickReactions = mutableListOf()
+
+    fun filterWith(query: String): List {
+        val words = query.split("\\s".toRegex())
+
+        // First add emojis with name matching query, sorted by name
+        return (rawData.emojis.values
+                .asSequence()
+                .filter { emojiItem ->
+                    emojiItem.name.contains(query, true)
+                }
+                .sortedBy { it.name } +
+                // Then emojis with keyword matching any of the word in the query, sorted by name
+                rawData.emojis.values
+                        .filter { emojiItem ->
+                            words.fold(true, { prev, word ->
+                                prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
+                            })
+                        }
+                        .sortedBy { it.name })
+                // and ensure they will not be present twice
+                .distinct()
+                .toList()
+    }
+
+    fun getQuickReactions(): List {
+        if (quickReactions.isEmpty()) {
+            listOf(
+                    "+1", // 👍
+                    "-1", // 👎
+                    "grinning", // 😄
+                    "tada", // 🎉
+                    "confused", // 😕
+                    "heart", // ❤️
+                    "rocket", // 🚀
+                    "eyes" // 👀
+            )
+                    .mapNotNullTo(quickReactions) { rawData.emojis[it] }
+        }
+
+        return quickReactions
+    }
+
+    companion object {
+        val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
+    }
 }
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/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt
index a83208c98a..2831457224 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt
@@ -22,6 +22,7 @@ import android.os.Bundle
 import androidx.appcompat.widget.Toolbar
 import com.airbnb.mvrx.viewModel
 import im.vector.riotx.R
+import im.vector.riotx.core.di.ScreenComponent
 import im.vector.riotx.core.extensions.addFragment
 import im.vector.riotx.core.platform.ToolbarConfigurable
 import im.vector.riotx.core.platform.VectorBaseActivity
@@ -45,6 +46,10 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable {
         configureToolbar(toolbar)
     }
 
+    override fun injectWith(injector: ScreenComponent) {
+        injector.inject(this)
+    }
+
     override fun initUiAndData() {
         if (isFirstCreation()) {
             addFragment(R.id.simpleFragmentContainer, CreateRoomFragment::class.java)
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt
index 17693d9ad6..4073929a4f 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt
@@ -48,6 +48,7 @@ class RoomDirectoryListCreator @Inject constructor(private val stringArrayProvid
             if (it != userHsName) {
                 // Use the server name as a default display name
                 result.add(RoomDirectoryData(
+                        homeServer = it,
                         displayName = it,
                         includeAllNetworks = true
                 ))
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/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
index dd99488465..72f8cf01dd 100755
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
@@ -23,6 +23,7 @@ import android.net.Uri
 import android.provider.MediaStore
 import androidx.core.content.edit
 import androidx.preference.PreferenceManager
+import com.squareup.seismic.ShakeDetector
 import im.vector.riotx.R
 import im.vector.riotx.features.homeserver.ServerUrlsRepository
 import im.vector.riotx.features.themes.ThemeUtils
@@ -62,8 +63,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
         const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY"
         const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
         const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY"
-        const val SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
-        const val SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY"
         const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY"
         const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY"
         const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"
@@ -149,12 +148,17 @@ class VectorPreferences @Inject constructor(private val context: Context) {
 
         const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
 
+        private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
         private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
         private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
+        private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY"
 
         // analytics
         const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
+
+        // Rageshake
         const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
+        const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
 
         // other
         const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"
@@ -247,8 +251,12 @@ class VectorPreferences @Inject constructor(private val context: Context) {
         }
     }
 
+    fun developerMode(): Boolean {
+        return defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY, false)
+    }
+
     fun shouldShowHiddenEvents(): Boolean {
-        return defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false)
+        return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false)
     }
 
     fun swipeToReplyIsEnabled(): Boolean {
@@ -256,7 +264,11 @@ class VectorPreferences @Inject constructor(private val context: Context) {
     }
 
     fun labAllowedExtendedLogging(): Boolean {
-        return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false)
+        return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false)
+    }
+
+    fun failFast(): Boolean {
+        return developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false)
     }
 
     /**
@@ -730,14 +742,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
     }
 
     /**
-     * Update the rage shake  status.
-     *
-     * @param isEnabled true to enable the rage shake
+     * Get the rage shake sensitivity.
      */
-    fun setUseRageshake(isEnabled: Boolean) {
-        defaultPrefs.edit {
-            putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled)
-        }
+    fun getRageshakeSensitivity(): Int {
+        return defaultPrefs.getInt(SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY, ShakeDetector.SENSITIVITY_MEDIUM)
     }
 
     /**
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt
index 16484224af..490805ea3c 100755
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt
@@ -54,7 +54,12 @@ class VectorSettingsActivity : VectorBaseActivity(),
 
         if (isFirstCreation()) {
             // display the fragment
-            replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
+            when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
+                EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
+                    replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
+                else                                  ->
+                    replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
+            }
         }
 
         supportFragmentManager.addOnBackStackChangedListener(this)
@@ -111,7 +116,13 @@ class VectorSettingsActivity : VectorBaseActivity(),
     }
 
     companion object {
-        fun getIntent(context: Context) = Intent(context, VectorSettingsActivity::class.java)
+        fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
+                .apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
+
+        private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS"
+
+        const val EXTRA_DIRECT_ACCESS_ROOT = 0
+        const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
 
         private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
     }
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt
new file mode 100644
index 0000000000..43adcf6335
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.settings
+
+import androidx.preference.Preference
+import androidx.preference.SeekBarPreference
+import im.vector.riotx.R
+import im.vector.riotx.core.platform.VectorBaseActivity
+import im.vector.riotx.core.preference.VectorSwitchPreference
+import im.vector.riotx.features.rageshake.RageShake
+
+class VectorSettingsAdvancedSettingsFragment : VectorSettingsBaseFragment() {
+
+    override var titleRes = R.string.settings_advanced_settings
+    override val preferenceXmlRes = R.xml.vector_settings_advanced_settings
+
+    private var rageshake: RageShake? = null
+
+    override fun onResume() {
+        super.onResume()
+
+        rageshake = (activity as? VectorBaseActivity)?.rageShake
+        rageshake?.interceptor = {
+            (activity as? VectorBaseActivity)?.showSnackbar(getString(R.string.rageshake_detected))
+        }
+    }
+
+    override fun onPause() {
+        super.onPause()
+        rageshake?.interceptor = null
+        rageshake = null
+    }
+
+    override fun bindPref() {
+        val isRageShakeAvailable = RageShake.isAvailable(requireContext())
+
+        if (isRageShakeAvailable) {
+            findPreference(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!!
+                    .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+
+                if (newValue as? Boolean == true) {
+                    rageshake?.start()
+                } else {
+                    rageshake?.stop()
+                }
+
+                true
+            }
+
+            findPreference(VectorPreferences.SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY)!!
+                    .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+                (activity as? VectorBaseActivity)?.let {
+                    val newValueAsInt = newValue as? Int ?: return@OnPreferenceChangeListener true
+
+                    rageshake?.setSensitivity(newValueAsInt)
+                }
+
+                true
+            }
+        } else {
+            findPreference("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt
index 6ce928c05d..6c10b8695d 100644
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt
@@ -20,13 +20,13 @@ import android.content.Intent
 import android.net.Uri
 import android.provider.Settings
 import androidx.preference.Preference
-import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
 import im.vector.matrix.android.api.Matrix
 import im.vector.riotx.R
 import im.vector.riotx.core.preference.VectorPreference
 import im.vector.riotx.core.utils.copyToClipboard
 import im.vector.riotx.core.utils.displayInWebView
 import im.vector.riotx.features.version.VersionProvider
+import im.vector.riotx.openOssLicensesMenuActivity
 import javax.inject.Inject
 
 class VectorSettingsHelpAboutFragment @Inject constructor(
@@ -107,10 +107,11 @@ class VectorSettingsHelpAboutFragment @Inject constructor(
             false
         }
 
+        // Note: preference is not visible on F-Droid build
         findPreference(VectorPreferences.SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY)!!
                 .onPreferenceClickListener = Preference.OnPreferenceClickListener {
             // See https://developers.google.com/android/guides/opensource
-            startActivity(Intent(requireActivity(), OssLicensesMenuActivity::class.java))
+            openOssLicensesMenuActivity(requireActivity())
             false
         }
     }
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
index b7ec443ea0..cf5273d5a4 100644
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
@@ -18,12 +18,8 @@ package im.vector.riotx.features.settings
 
 import android.annotation.SuppressLint
 import android.app.Activity
-import android.content.DialogInterface
 import android.content.Intent
-import android.graphics.Typeface
-import android.view.KeyEvent
 import android.widget.Button
-import android.widget.EditText
 import android.widget.TextView
 import androidx.appcompat.app.AlertDialog
 import androidx.core.view.isVisible
@@ -33,30 +29,19 @@ import androidx.preference.SwitchPreference
 import com.google.android.material.textfield.TextInputEditText
 import im.vector.matrix.android.api.MatrixCallback
 import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
-import im.vector.matrix.android.api.extensions.sortByLastSeen
-import im.vector.matrix.android.api.failure.Failure
-import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
 import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
 import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
-import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
 import im.vector.riotx.R
 import im.vector.riotx.core.dialogs.ExportKeysDialog
 import im.vector.riotx.core.intent.ExternalIntentData
 import im.vector.riotx.core.intent.analyseIntent
 import im.vector.riotx.core.intent.getFilenameFromUri
 import im.vector.riotx.core.platform.SimpleTextWatcher
-import im.vector.riotx.core.preference.ProgressBarPreference
 import im.vector.riotx.core.preference.VectorPreference
-import im.vector.riotx.core.preference.VectorPreferenceDivider
 import im.vector.riotx.core.utils.*
 import im.vector.riotx.features.crypto.keys.KeysExporter
 import im.vector.riotx.features.crypto.keys.KeysImporter
 import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
-import timber.log.Timber
-import java.text.DateFormat
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
 import javax.inject.Inject
 
 class VectorSettingsSecurityPrivacyFragment @Inject constructor(
@@ -66,9 +51,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     override var titleRes = R.string.settings_security_and_privacy
     override val preferenceXmlRes = R.xml.vector_settings_security_privacy
 
-    // used to avoid requesting to enter the password for each deletion
-    private var mAccountPassword: String = ""
-
     // devices: device IDs and device names
     private val mDevicesNameList: MutableList = mutableListOf()
 
@@ -78,29 +60,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     private val mCryptographyCategory by lazy {
         findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
     }
-    private val mCryptographyCategoryDivider by lazy {
-        findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY)!!
-    }
     // cryptography manage
     private val mCryptographyManageCategory by lazy {
         findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!!
     }
-    private val mCryptographyManageCategoryDivider by lazy {
-        findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY)!!
-    }
     // displayed pushers
-    private val mPushersSettingsDivider by lazy {
-        findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY)!!
-    }
     private val mPushersSettingsCategory by lazy {
         findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!!
     }
-    private val mDevicesListSettingsCategory by lazy {
-        findPreference(VectorPreferences.SETTINGS_DEVICES_LIST_PREFERENCE_KEY)!!
-    }
-    private val mDevicesListSettingsCategoryDivider by lazy {
-        findPreference(VectorPreferences.SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY)!!
-    }
     private val cryptoInfoDeviceNamePreference by lazy {
         findPreference(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!!
     }
@@ -129,13 +96,16 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         findPreference(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
     }
 
+    override fun onResume() {
+        super.onResume()
+        // My device name may have been updated
+        refreshMyDevice()
+    }
+
     override fun bindPref() {
         // Push target
         refreshPushersList()
 
-        // Device list
-        refreshDevicesList()
-
         // Refresh Key Management section
         refreshKeysManagementSection()
 
@@ -151,16 +121,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                 true
             }
         }
-
-        // Rageshake Management
-        findPreference(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!!.let {
-            it.isChecked = vectorPreferences.useRageshake()
-
-            it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
-                vectorPreferences.setUseRageshake(newValue as Boolean)
-                true
-            }
-        }
     }
 
     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
@@ -353,11 +313,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     private fun removeCryptographyPreference() {
         preferenceScreen.let {
             it.removePreference(mCryptographyCategory)
-            it.removePreference(mCryptographyCategoryDivider)
 
             // Also remove keys management section
             it.removePreference(mCryptographyManageCategory)
-            it.removePreference(mCryptographyManageCategoryDivider)
         }
     }
 
@@ -375,7 +333,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
 
             cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
-                displayDeviceRenameDialog(aMyDeviceInfo)
+                // TODO device can be rename only from the device list screen for the moment
+                // displayDeviceRenameDialog(aMyDeviceInfo)
                 true
             }
 
@@ -428,342 +387,22 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     // devices list
     // ==============================================================================================================
 
-    private fun removeDevicesPreference() {
-        preferenceScreen.let {
-            it.removePreference(mDevicesListSettingsCategory)
-            it.removePreference(mDevicesListSettingsCategoryDivider)
-        }
-    }
-
-    /**
-     * Force the refresh of the devices list.

- * The devices list is the list of the devices where the user as looged in. - * It can be any mobile device, as any browser. - */ - private fun refreshDevicesList() { - if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) { - // display a spinner while loading the devices list - if (0 == mDevicesListSettingsCategory.preferenceCount) { - activity?.let { - val preference = ProgressBarPreference(it) - mDevicesListSettingsCategory.addPreference(preference) - } - } - - session.getDevicesList(object : MatrixCallback { - override fun onSuccess(data: DevicesListResponse) { - if (!isAdded) { - return - } - - if (data.devices?.isEmpty() == true) { - removeDevicesPreference() - } else { - buildDevicesSettings(data.devices!!) - } - } - + private fun refreshMyDevice() { + // TODO Move to a ViewModel... + session.sessionParams.credentials.deviceId?.let { + session.getDeviceInfo(it, object : MatrixCallback { override fun onFailure(failure: Throwable) { - if (!isAdded) { - return - } + // Ignore for this time?... + } - removeDevicesPreference() - onCommonDone(failure.message) + override fun onSuccess(data: DeviceInfo) { + mMyDeviceInfo = data + refreshCryptographyPreference(data) } }) - } else { - removeDevicesPreference() - removeCryptographyPreference() } } - /** - * Build the devices portion of the settings.

- * Each row correspond to a device ID and its corresponding device name. Clicking on the row - * display a dialog containing: the device ID, the device name and the "last seen" information. - * - * @param aDeviceInfoList the list of the devices - */ - private fun buildDevicesSettings(aDeviceInfoList: List) { - var preference: VectorPreference - var typeFaceHighlight: Int - var isNewList = true - val myDeviceId = session.sessionParams.credentials.deviceId - - if (aDeviceInfoList.size == mDevicesNameList.size) { - isNewList = !mDevicesNameList.containsAll(aDeviceInfoList) - } - - if (isNewList) { - var prefIndex = 0 - mDevicesNameList.clear() - mDevicesNameList.addAll(aDeviceInfoList) - - // sort before display: most recent first - mDevicesNameList.sortByLastSeen() - - // start from scratch: remove the displayed ones - mDevicesListSettingsCategory.removeAll() - - for (deviceInfo in mDevicesNameList) { - // set bold to distinguish current device ID - if (null != myDeviceId && myDeviceId == deviceInfo.deviceId) { - mMyDeviceInfo = deviceInfo - typeFaceHighlight = Typeface.BOLD - } else { - typeFaceHighlight = Typeface.NORMAL - } - - // add the edit text preference - preference = VectorPreference(requireActivity()).apply { - mTypeface = typeFaceHighlight - } - - if (null == deviceInfo.deviceId && null == deviceInfo.displayName) { - continue - } else { - if (null != deviceInfo.deviceId) { - preference.title = deviceInfo.deviceId - } - - // display name parameter can be null (new JSON API) - if (null != deviceInfo.displayName) { - preference.summary = deviceInfo.displayName - } - } - - preference.key = DEVICES_PREFERENCE_KEY_BASE + prefIndex - prefIndex++ - - // onClick handler: display device details dialog - preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - displayDeviceDetailsDialog(deviceInfo) - true - } - - mDevicesListSettingsCategory.addPreference(preference) - } - - refreshCryptographyPreference(mMyDeviceInfo) - } - } - - /** - * Display a dialog containing the device ID, the device name and the "last seen" information.<> - * This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog]) - * - * @param aDeviceInfo the device information - */ - private fun displayDeviceDetailsDialog(aDeviceInfo: DeviceInfo) { - activity?.let { - val builder = AlertDialog.Builder(it) - val inflater = it.layoutInflater - val layout = inflater.inflate(R.layout.dialog_device_details, null) - var textView = layout.findViewById(R.id.device_id) - - textView.text = aDeviceInfo.deviceId - - // device name - textView = layout.findViewById(R.id.device_name) - val displayName = if (aDeviceInfo.displayName.isNullOrEmpty()) LABEL_UNAVAILABLE_DATA else aDeviceInfo.displayName - textView.text = displayName - - // last seen info - textView = layout.findViewById(R.id.device_last_seen) - - val lastSeenIp = aDeviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" - - val lastSeenTime = aDeviceInfo.lastSeenTs?.let { ts -> - val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT) - val date = Date(ts) - - val time = dateFormatTime.format(date) - val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()) - - dateFormat.format(date) + ", " + time - } ?: "-" - - val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) - textView.text = lastSeenInfo - - // title & icon - builder.setTitle(R.string.devices_details_dialog_title) - .setIcon(android.R.drawable.ic_dialog_info) - .setView(layout) - .setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) } - - // disable the deletion for our own device - if (session.getMyDevice().deviceId != aDeviceInfo.deviceId) { - builder.setNegativeButton(R.string.delete) { _, _ -> deleteDevice(aDeviceInfo) } - } - - builder.setNeutralButton(R.string.cancel, null) - .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> - if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - dialog.cancel() - return@OnKeyListener true - } - false - }) - .show() - } - } - - /** - * Display an alert dialog to rename a device - * - * @param aDeviceInfoToRename device info - */ - private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) { - activity?.let { - val inflater = it.layoutInflater - val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) - - val input = layout.findViewById(R.id.edit_text) - input.setText(aDeviceInfoToRename.displayName) - - AlertDialog.Builder(it) - .setTitle(R.string.devices_details_device_name) - .setView(layout) - .setPositiveButton(R.string.ok) { _, _ -> - displayLoadingView() - - val newName = input.text.toString() - - session.setDeviceName(aDeviceInfoToRename.deviceId!!, newName, object : MatrixCallback { - override fun onSuccess(data: Unit) { - hideLoadingView() - - // search which preference is updated - val count = mDevicesListSettingsCategory.preferenceCount - - for (i in 0 until count) { - val pref = mDevicesListSettingsCategory.getPreference(i) - - if (aDeviceInfoToRename.deviceId == pref.title) { - pref.summary = newName - } - } - - // detect if the updated device is the current account one - if (cryptoInfoDeviceIdPreference.summary == aDeviceInfoToRename.deviceId) { - cryptoInfoDeviceNamePreference.summary = newName - } - - // Also change the display name in aDeviceInfoToRename, in case of multiple renaming - aDeviceInfoToRename.displayName = newName - } - - override fun onFailure(failure: Throwable) { - onCommonDone(failure.localizedMessage) - } - }) - } - .setNegativeButton(R.string.cancel, null) - .show() - } - } - - /** - * Try to delete a device. - * - * @param deviceInfo the device to delete - */ - private fun deleteDevice(deviceInfo: DeviceInfo) { - val deviceId = deviceInfo.deviceId - if (deviceId == null) { - Timber.e("## displayDeviceDeletionDialog(): sanity check failure") - return - } - - displayLoadingView() - session.deleteDevice(deviceId, object : MatrixCallback { - override fun onSuccess(data: Unit) { - hideLoadingView() - // force settings update - refreshDevicesList() - } - - override fun onFailure(failure: Throwable) { - var isPasswordRequestFound = false - - if (failure is Failure.RegistrationFlowError) { - // We only support LoginFlowTypes.PASSWORD - // Check if we can provide the user password - failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> - isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true - } - - if (isPasswordRequestFound) { - maybeShowDeleteDeviceWithPasswordDialog(deviceId, failure.registrationFlowResponse.session) - } - } - - if (!isPasswordRequestFound) { - // LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far... - onCommonDone(failure.localizedMessage) - } - } - }) - } - - /** - * Show a dialog to ask for user password, or use a previously entered password. - */ - private fun maybeShowDeleteDeviceWithPasswordDialog(deviceId: String, authSession: String?) { - if (mAccountPassword.isNotEmpty()) { - deleteDeviceWithPassword(deviceId, authSession, mAccountPassword) - } else { - activity?.let { - val inflater = it.layoutInflater - val layout = inflater.inflate(R.layout.dialog_device_delete, null) - val passwordEditText = layout.findViewById(R.id.delete_password) - - AlertDialog.Builder(it) - .setIcon(android.R.drawable.ic_dialog_alert) - .setTitle(R.string.devices_delete_dialog_title) - .setView(layout) - .setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ -> - if (passwordEditText.toString().isEmpty()) { - it.toast(R.string.error_empty_field_your_password) - return@OnClickListener - } - mAccountPassword = passwordEditText.text.toString() - deleteDeviceWithPassword(deviceId, authSession, mAccountPassword) - }) - .setNegativeButton(R.string.cancel) { _, _ -> - hideLoadingView() - } - .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> - if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - dialog.cancel() - hideLoadingView() - return@OnKeyListener true - } - false - }) - .show() - } - } - } - - private fun deleteDeviceWithPassword(deviceId: String, authSession: String?, accountPassword: String) { - session.deleteDeviceWithUserPassword(deviceId, authSession, accountPassword, object : MatrixCallback { - override fun onSuccess(data: Unit) { - hideLoadingView() - // force settings update - refreshDevicesList() - } - - override fun onFailure(failure: Throwable) { - // Password is maybe not good - onCommonDone(failure.localizedMessage) - mAccountPassword = "" - } - }) - } - // ============================================================================================================== // pushers list management // ============================================================================================================== @@ -860,6 +499,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE" // TODO i18n - private const val LABEL_UNAVAILABLE_DATA = "none" + const val LABEL_UNAVAILABLE_DATA = "none" } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt new file mode 100644 index 0000000000..b6c84ade9a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt @@ -0,0 +1,109 @@ +/* + * 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.settings.devices + +import android.graphics.Typeface +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +/** + * A list item for Device. + */ +@EpoxyModelClass(layout = R.layout.item_device) +abstract class DeviceItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var deviceInfo: DeviceInfo + + @EpoxyAttribute + var currentDevice = false + + @EpoxyAttribute + var buttonsVisible = false + + @EpoxyAttribute + var itemClickAction: (() -> Unit)? = null + + @EpoxyAttribute + var renameClickAction: (() -> Unit)? = null + + @EpoxyAttribute + var deleteClickAction: (() -> Unit)? = null + + override fun bind(holder: Holder) { + holder.root.setOnClickListener { itemClickAction?.invoke() } + + holder.displayNameText.text = deviceInfo.displayName ?: "" + holder.deviceIdText.text = deviceInfo.deviceId ?: "" + + val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" + + val lastSeenTime = deviceInfo.lastSeenTs?.let { ts -> + val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT) + val date = Date(ts) + + val time = dateFormatTime.format(date) + val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()) + + dateFormat.format(date) + ", " + time + } ?: "-" + + holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) + + listOf( + holder.displayNameLabelText, + holder.displayNameText, + holder.deviceIdLabelText, + holder.deviceIdText, + holder.deviceLastSeenLabelText, + holder.deviceLastSeenText + ).map { + it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL) + } + + holder.buttonDelete.isVisible = !currentDevice + + holder.buttons.isVisible = buttonsVisible + + holder.buttonRename.setOnClickListener { renameClickAction?.invoke() } + holder.buttonDelete.setOnClickListener { deleteClickAction?.invoke() } + } + + class Holder : VectorEpoxyHolder() { + val root by bind(R.id.itemDeviceRoot) + val displayNameLabelText by bind(R.id.itemDeviceDisplayNameLabel) + val displayNameText by bind(R.id.itemDeviceDisplayName) + val deviceIdLabelText by bind(R.id.itemDeviceIdLabel) + val deviceIdText by bind(R.id.itemDeviceId) + val deviceLastSeenLabelText by bind(R.id.itemDeviceLastSeenLabel) + val deviceLastSeenText by bind(R.id.itemDeviceLastSeen) + val buttons by bind(R.id.itemDeviceButtons) + val buttonDelete by bind(R.id.itemDeviceDelete) + val buttonRename by bind(R.id.itemDeviceRename) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt new file mode 100644 index 0000000000..18c0965f86 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -0,0 +1,129 @@ +/* + * 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.settings.devices + +import com.airbnb.epoxy.EpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.extensions.sortByLastSeen +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.ui.list.genericItemHeader +import javax.inject.Inject + +class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter, + private val stringProvider: StringProvider) : EpoxyController() { + + var callback: Callback? = null + private var viewState: DevicesViewState? = null + + init { + requestModelBuild() + } + + fun update(viewState: DevicesViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val nonNullViewState = viewState ?: return + buildDevicesModels(nonNullViewState) + } + + private fun buildDevicesModels(state: DevicesViewState) { + when (val devices = state.devices) { + is Loading, + is Uninitialized -> + loadingItem { + id("loading") + } + is Fail -> + errorWithRetryItem { + id("error") + text(errorFormatter.toHumanReadable(devices.error)) + listener { callback?.retry() } + } + is Success -> + buildDevicesList(devices(), state.myDeviceId, state.currentExpandedDeviceId) + } + } + + private fun buildDevicesList(devices: List, myDeviceId: String, currentExpandedDeviceId: String?) { + // Current device + genericItemHeader { + id("current") + text(stringProvider.getString(R.string.devices_current_device)) + } + + devices + .filter { + it.deviceId == myDeviceId + } + .forEachIndexed { idx, deviceInfo -> + deviceItem { + id("myDevice$idx") + deviceInfo(deviceInfo) + currentDevice(true) + buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId) + itemClickAction { callback?.onDeviceClicked(deviceInfo) } + renameClickAction { callback?.onRenameDevice(deviceInfo) } + deleteClickAction { callback?.onDeleteDevice(deviceInfo) } + } + } + + // Other devices + if (devices.size > 1) { + genericItemHeader { + id("others") + text(stringProvider.getString(R.string.devices_other_devices)) + } + + devices + .filter { + it.deviceId != myDeviceId + } + // sort before display: most recent first + .sortByLastSeen() + .forEachIndexed { idx, deviceInfo -> + val isCurrentDevice = deviceInfo.deviceId == myDeviceId + deviceItem { + id("device$idx") + deviceInfo(deviceInfo) + currentDevice(isCurrentDevice) + buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId) + itemClickAction { callback?.onDeviceClicked(deviceInfo) } + renameClickAction { callback?.onRenameDevice(deviceInfo) } + deleteClickAction { callback?.onDeleteDevice(deviceInfo) } + } + } + } + } + + interface Callback { + fun retry() + fun onDeviceClicked(deviceInfo: DeviceInfo) + fun onRenameDevice(deviceInfo: DeviceInfo) + fun onDeleteDevice(deviceInfo: DeviceInfo) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt new file mode 100644 index 0000000000..b2b015a3f0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -0,0 +1,268 @@ +/* + * 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.settings.devices + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.* +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.failure.Failure +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse +import im.vector.riotx.core.extensions.postLiveEvent +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.core.utils.LiveEvent +import timber.log.Timber + +data class DevicesViewState( + val myDeviceId: String = "", + val devices: Async> = Uninitialized, + val currentExpandedDeviceId: String? = null, + val request: Async = Uninitialized +) : MvRxState + +sealed class DevicesAction : VectorViewModelAction { + object Retry : DevicesAction() + data class Delete(val deviceInfo: DeviceInfo) : DevicesAction() + data class Password(val password: String) : DevicesAction() + data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction() + data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction() +} + +class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DevicesViewState): DevicesViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: DevicesViewState): DevicesViewModel? { + val fragment: VectorSettingsDevicesFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.devicesViewModelFactory.create(state) + } + } + + // temp storage when we ask for the user password + private var _currentDeviceId: String? = null + private var _currentSession: String? = null + + private val _requestPasswordLiveData = MutableLiveData>() + val requestPasswordLiveData: LiveData> + get() = _requestPasswordLiveData + + init { + refreshDevicesList() + } + + /** + * Force the refresh of the devices list. + * The devices list is the list of the devices where the user is logged in. + * It can be any mobile devices, and any browsers. + */ + private fun refreshDevicesList() { + if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) { + setState { + copy( + devices = Loading() + ) + } + + session.getDevicesList(object : MatrixCallback { + override fun onSuccess(data: DevicesListResponse) { + setState { + copy( + myDeviceId = session.sessionParams.credentials.deviceId ?: "", + devices = Success(data.devices.orEmpty()) + ) + } + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + devices = Fail(failure) + ) + } + } + }) + } else { + // Should not happen + } + } + + override fun handle(action: DevicesAction) { + return when (action) { + is DevicesAction.Retry -> refreshDevicesList() + is DevicesAction.Delete -> handleDelete(action) + is DevicesAction.Password -> handlePassword(action) + is DevicesAction.Rename -> handleRename(action) + is DevicesAction.ToggleDevice -> handleToggleDevice(action) + } + } + + private fun handleToggleDevice(action: DevicesAction.ToggleDevice) { + withState { + setState { + copy( + currentExpandedDeviceId = if (it.currentExpandedDeviceId == action.deviceInfo.deviceId) null else action.deviceInfo.deviceId + ) + } + } + } + + private fun handleRename(action: DevicesAction.Rename) { + session.setDeviceName(action.deviceInfo.deviceId!!, action.newName, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { + copy( + request = Success(data) + ) + } + // force settings update + refreshDevicesList() + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + request = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + }) + } + + /** + * Try to delete a device. + */ + private fun handleDelete(action: DevicesAction.Delete) { + val deviceId = action.deviceInfo.deviceId + if (deviceId == null) { + Timber.e("## handleDelete(): sanity check failure") + return + } + + setState { + copy( + request = Loading() + ) + } + + session.deleteDevice(deviceId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + var isPasswordRequestFound = false + + if (failure is Failure.RegistrationFlowError) { + // We only support LoginFlowTypes.PASSWORD + // Check if we can provide the user password + failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> + isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true + } + + if (isPasswordRequestFound) { + _currentDeviceId = deviceId + _currentSession = failure.registrationFlowResponse.session + + setState { + copy( + request = Success(Unit) + ) + } + + _requestPasswordLiveData.postLiveEvent(Unit) + } + } + + if (!isPasswordRequestFound) { + // LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far... + setState { + copy( + request = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + } + + override fun onSuccess(data: Unit) { + setState { + copy( + request = Success(data) + ) + } + // force settings update + refreshDevicesList() + } + }) + } + + private fun handlePassword(action: DevicesAction.Password) { + val currentDeviceId = _currentDeviceId + if (currentDeviceId.isNullOrBlank()) { + // Abort + return + } + + setState { + copy( + request = Loading() + ) + } + + session.deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback { + override fun onSuccess(data: Unit) { + _currentDeviceId = null + _currentSession = null + + setState { + copy( + request = Success(data) + ) + } + // force settings update + refreshDevicesList() + } + + override fun onFailure(failure: Throwable) { + _currentDeviceId = null + _currentSession = null + + // Password is maybe not good + setState { + copy( + request = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + }) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt new file mode 100644 index 0000000000..465b3ba0fb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -0,0 +1,181 @@ +/* + * 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.settings.devices + +import android.content.DialogInterface +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.toast +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import javax.inject.Inject + +/** + * Display the list of the user's device + */ +class VectorSettingsDevicesFragment @Inject constructor( + val devicesViewModelFactory: DevicesViewModel.Factory, + private val devicesController: DevicesController +) : VectorBaseFragment(), DevicesController.Callback { + + // used to avoid requesting to enter the password for each deletion + private var mAccountPassword: String = "" + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + private val devicesViewModel: DevicesViewModel by fragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + waiting_view_status_text.setText(R.string.please_wait) + waiting_view_status_text.isVisible = true + devicesController.callback = this + recyclerView.configureWith(devicesController, showDivider = true) + devicesViewModel.requestErrorLiveData.observeEvent(this) { + displayErrorDialog(it) + // Password is maybe not good, for safety measure, reset it here + mAccountPassword = "" + } + devicesViewModel.requestPasswordLiveData.observeEvent(this) { + maybeShowDeleteDeviceWithPasswordDialog() + } + } + + override fun onDestroyView() { + devicesController.callback = null + recyclerView.cleanup() + super.onDestroyView() + } + + override fun onResume() { + super.onResume() + + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_devices_list) + } + + private fun displayErrorDialog(throwable: Throwable) { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(throwable)) + .setPositiveButton(R.string.ok, null) + .show() + } + + override fun onDeviceClicked(deviceInfo: DeviceInfo) { + devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo)) + } + + override fun onDeleteDevice(deviceInfo: DeviceInfo) { + devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) + } + + override fun onRenameDevice(deviceInfo: DeviceInfo) { + displayDeviceRenameDialog(deviceInfo) + } + + override fun retry() { + devicesViewModel.handle(DevicesAction.Retry) + } + + /** + * Display an alert dialog to rename a device + * + * @param deviceInfo device info + */ + private fun displayDeviceRenameDialog(deviceInfo: DeviceInfo) { + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) + + val input = layout.findViewById(R.id.edit_text) + input.setText(deviceInfo.displayName) + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.devices_details_device_name) + .setView(layout) + .setPositiveButton(R.string.ok) { _, _ -> + val newName = input.text.toString() + + devicesViewModel.handle(DevicesAction.Rename(deviceInfo, newName)) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + /** + * Show a dialog to ask for user password, or use a previously entered password. + */ + private fun maybeShowDeleteDeviceWithPasswordDialog() { + if (mAccountPassword.isNotEmpty()) { + devicesViewModel.handle(DevicesAction.Password(mAccountPassword)) + } else { + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_device_delete, null) + val passwordEditText = layout.findViewById(R.id.delete_password) + + AlertDialog.Builder(requireActivity()) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.devices_delete_dialog_title) + .setView(layout) + .setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ -> + if (passwordEditText.toString().isEmpty()) { + requireActivity().toast(R.string.error_empty_field_your_password) + return@OnClickListener + } + mAccountPassword = passwordEditText.text.toString() + devicesViewModel.handle(DevicesAction.Password(mAccountPassword)) + }) + .setNegativeButton(R.string.cancel, null) + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) + .show() + } + } + + override fun invalidate() = withState(devicesViewModel) { state -> + devicesController.update(state) + + handleRequestStatus(state.request) + } + + private fun handleRequestStatus(unIgnoreRequest: Async) { + when (unIgnoreRequest) { + is Loading -> waiting_view.isVisible = true + else -> waiting_view.isVisible = false + } + } +} 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/bottom_sheet_generic_list.xml b/vector/src/main/res/layout/bottom_sheet_generic_list.xml index 69b5ce2fac..0bd6665325 100644 --- a/vector/src/main/res/layout/bottom_sheet_generic_list.xml +++ b/vector/src/main/res/layout/bottom_sheet_generic_list.xml @@ -1,5 +1,4 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_public_rooms.xml b/vector/src/main/res/layout/fragment_public_rooms.xml index 7bf4bfaf37..de448f3b8b 100644 --- a/vector/src/main/res/layout/fragment_public_rooms.xml +++ b/vector/src/main/res/layout/fragment_public_rooms.xml @@ -1,5 +1,4 @@ - - - + android:foreground="?attr/selectableItemBackground" + android:paddingStart="8dp" + android:paddingTop="6dp" + android:paddingEnd="8dp" + android:paddingBottom="6dp"> diff --git a/vector/src/main/res/layout/item_autocomplete_emoji.xml b/vector/src/main/res/layout/item_autocomplete_emoji.xml new file mode 100644 index 0000000000..c34ab0d452 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_emoji.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_autocomplete_matrix_item.xml b/vector/src/main/res/layout/item_autocomplete_matrix_item.xml new file mode 100644 index 0000000000..84a85d0102 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_matrix_item.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_autocomplete_more_result.xml b/vector/src/main/res/layout/item_autocomplete_more_result.xml new file mode 100644 index 0000000000..d04f515ed0 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_more_result.xml @@ -0,0 +1,9 @@ + + diff --git a/vector/src/main/res/layout/item_autocomplete_user.xml b/vector/src/main/res/layout/item_autocomplete_user.xml deleted file mode 100644 index f2fdb354a9..0000000000 --- a/vector/src/main/res/layout/item_autocomplete_user.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - \ No newline at end of file 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 @@ diff --git a/vector/src/main/res/layout/item_device.xml b/vector/src/main/res/layout/item_device.xml new file mode 100644 index 0000000000..bebaf156d9 --- /dev/null +++ b/vector/src/main/res/layout/item_device.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml b/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml index 4738f331b1..7889efacc4 100644 --- a/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml @@ -1,5 +1,4 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/vector_settings_address_preference.xml b/vector/src/main/res/layout/vector_settings_address_preference.xml index 97b7e119f8..b22fa38ba8 100644 --- a/vector/src/main/res/layout/vector_settings_address_preference.xml +++ b/vector/src/main/res/layout/vector_settings_address_preference.xml @@ -1,5 +1,4 @@ - diff --git a/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml b/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml index 137d96408d..548594fcd4 100644 --- a/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml +++ b/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml @@ -1,5 +1,4 @@ - diff --git a/vector/src/main/res/layout/vector_settings_round_avatar.xml b/vector/src/main/res/layout/vector_settings_round_avatar.xml index 66c2d2412b..fba69dc588 100644 --- a/vector/src/main/res/layout/vector_settings_round_avatar.xml +++ b/vector/src/main/res/layout/vector_settings_round_avatar.xml @@ -1,5 +1,4 @@ - - diff --git a/vector/src/main/res/layout/view_attachment_type_selector.xml b/vector/src/main/res/layout/view_attachment_type_selector.xml index f713561084..134ad47c92 100644 --- a/vector/src/main/res/layout/view_attachment_type_selector.xml +++ b/vector/src/main/res/layout/view_attachment_type_selector.xml @@ -1,5 +1,4 @@ - - - - - diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 43ed38915b..fdfd8f85ef 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -3,6 +3,25 @@ + Initial Sync… + + + See all my devices + Advanced settings + Developer mode + The developer mode activates hidden features and may also make the application less stable. For developers only! + Rageshake + Detection threshold + Shake your phone to test the detection threshold + Shake detected! + Settings + Current device + Other devices + + Showing only the first results, type more letters… + + Fail-fast + RiotX may crash more often when an unexpected error occurs Messages in this room are not end-to-end encrypted. Messages in this room are end-to-end encrypted. diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index 7398a4bcb7..7bce009429 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -72,9 +72,6 @@ @color/primary_color_dark_black @color/list_divider_color_black - - @color/list_divider_color_black - #FF4D4D4D @drawable/pill_receipt_black diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index f61a89482a..a05081eec7 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -139,9 +139,6 @@ #FF61708b - - @color/list_divider_color_dark - @android:color/white @color/riotx_accent diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index aa343a11fc..9cea0e52b7 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -139,9 +139,6 @@ #FF61708b - - @color/list_divider_color_light - @android:color/black @color/riotx_accent diff --git a/vector/src/main/res/values/theme_status.xml b/vector/src/main/res/values/theme_status.xml index 322522c723..421632e64c 100644 --- a/vector/src/main/res/values/theme_status.xml +++ b/vector/src/main/res/values/theme_status.xml @@ -88,9 +88,6 @@ #a0a29f - - #e1e1e1 - @color/accent_color_status @color/riotx_accent diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml new file mode 100644 index 0000000000..131b43c8d5 --- /dev/null +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index d1ffe5bcf1..c49eca825a 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -45,11 +45,10 @@ - - + android:title="@string/settings_contact" + app:isPreferenceVisible="@bool/false_not_implemented"> - - + tools:summary="https://homeserver.org" /> - - - + + android:title="@string/settings_deactivate_account_section" + app:isPreferenceVisible="@bool/false_not_implemented"> - + - + - + - + - + - + - + - + - - - - - + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index e9e5e27198..2661568f77 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -34,24 +34,12 @@ - - - - - \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml b/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml index 32b6a2b499..b5f01d98f6 100644 --- a/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml @@ -36,8 +36,6 @@ - - - - + android:title="@string/settings_notifications_targets" /--> - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 96471cfebe..7698372053 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -30,13 +30,15 @@ android:defaultValue="true" android:key="SETTINGS_SHOW_URL_PREVIEW_KEY" android:summary="@string/settings_inline_url_preview_summary" - android:title="@string/settings_inline_url_preview" /> + android:title="@string/settings_inline_url_preview" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_send_typing_notifs" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_always_show_timestamps" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_12_24_timestamps" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_read_receipts" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_join_leave_messages" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_avatar_display_name_changes_messages" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_vibrate_on_mention" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_send_message_with_enter" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_info_area_show" + app:isPreferenceVisible="@bool/false_not_implemented" /> - - + android:title="@string/settings_home_display" + app:isPreferenceVisible="@bool/false_not_implemented"> - - - + + + diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index 9e88da34a1..234ecbe647 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -1,5 +1,6 @@ - + + android:title="@string/encryption_never_send_to_unverified_devices_title" + app:isPreferenceVisible="@bool/false_not_implemented" /> - + + + + + + - - - - - - - + android:title="@string/settings_analytics" + app:isPreferenceVisible="@bool/false_not_implemented"> - \ No newline at end of file