From d1b8d81fb168216451b14c3849c72e131f619d2a Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Jan 2020 14:17:04 +0100 Subject: [PATCH 01/34] Fix double read receipts --- .idea/codeStyles/codeStyleConfig.xml | 1 + .../session/room/timeline/DefaultTimeline.kt | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 79ee123c2b..6e6eec1148 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ \ No newline at end of file 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 057295ec44..3ca4062161 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,7 +27,13 @@ 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.* +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.query.FilterContent import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where @@ -38,14 +44,19 @@ 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.* +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 timber.log.Timber -import java.util.* +import java.util.Collections +import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -import kotlin.collections.ArrayList -import kotlin.collections.HashMap import kotlin.math.max import kotlin.math.min @@ -171,7 +182,7 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } - if (settings.buildReadReceipts) { + if (settings.shouldHandleHiddenReadReceipts()) { hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) } isReady.set(true) @@ -179,6 +190,10 @@ internal class DefaultTimeline( } } + private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { + return settings.buildReadReceipts && (settings.filterEdits || settings.filterTypes) + } + override fun dispose() { if (isStarted.compareAndSet(true, false)) { isReady.set(false) @@ -193,7 +208,7 @@ internal class DefaultTimeline( if (this::filteredEvents.isInitialized) { filteredEvents.removeAllChangeListeners() } - if (settings.buildReadReceipts) { + if (settings.shouldHandleHiddenReadReceipts()) { hiddenReadReceipts.dispose() } clearAllValues() From 2bddf61afeee0615d4c40089a76111305461b221 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Jan 2020 15:15:29 +0100 Subject: [PATCH 02/34] Update realm to 6.1.0: should fix some of the native crashes --- matrix-sdk-android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 7a1348a54c..e97405125f 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:6.0.2" + classpath "io.realm:realm-gradle-plugin:6.1.0" } } From d93050240a3e2994452f207bc29590e160a01ba1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Jan 2020 18:48:19 +0100 Subject: [PATCH 03/34] Start reworking networkConnectivityCheck (WIP) --- .../android/internal/di/MatrixComponent.kt | 2 - .../network/NetworkConnectivityChecker.kt | 233 +++++++++++------- .../android/internal/session/SessionModule.kt | 5 + .../room/timeline/DefaultPaginationTask.kt | 5 +- .../session/room/timeline/DefaultTimeline.kt | 2 - .../internal/session/sync/job/SyncService.kt | 22 +- .../internal/session/sync/job/SyncThread.kt | 19 +- .../android/internal/task/ConfigurableTask.kt | 3 - .../android/internal/task/TaskConstraints.kt | 22 -- .../android/internal/task/TaskExecutor.kt | 14 +- .../riotx/core/services/VectorSyncService.kt | 17 +- 11 files changed, 189 insertions(+), 155 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt index e8fa659d8d..a912e421df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt @@ -56,8 +56,6 @@ internal interface MatrixComponent { fun sessionParamsStore(): SessionParamsStore - fun networkConnectivityChecker(): NetworkConnectivityChecker - fun backgroundDetectionObserver(): BackgroundDetectionObserver fun sessionManager(): SessionManager 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 6565b8685b..4af6fe8b1c 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 @@ -18,104 +18,36 @@ package im.vector.matrix.android.internal.network import android.content.Context import androidx.annotation.WorkerThread +import com.novoda.merlin.Endpoint import com.novoda.merlin.Merlin import com.novoda.merlin.MerlinsBeard -import im.vector.matrix.android.internal.di.MatrixScope +import com.novoda.merlin.ResponseCodeValidator +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.BackgroundDetectionObserver +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Collections +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -@MatrixScope -internal class NetworkConnectivityChecker @Inject constructor(private val context: Context, - private val backgroundDetectionObserver: BackgroundDetectionObserver) - : BackgroundDetectionObserver.Listener { - - private val merlin = Merlin.Builder() - .withConnectableCallbacks() - .withDisconnectableCallbacks() - .build(context) - - private val merlinsBeard = MerlinsBeard.Builder().build(context) - - private val listeners = Collections.synchronizedSet(LinkedHashSet()) - private var hasInternetAccess = merlinsBeard.isConnected - - init { - backgroundDetectionObserver.register(this) - } - +interface NetworkConnectivityChecker { /** * 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 - } - } + fun hasInternetAccess(forcePing: Boolean): Boolean - override fun onMoveToForeground() { - merlin.bind() - merlinsBeard.hasInternetAccess { - hasInternetAccess = it - } - merlin.registerDisconnectable { - if (hasInternetAccess) { - Timber.v("On Disconnect") - hasInternetAccess = false - val localListeners = listeners.toList() - localListeners.forEach { - it.onDisconnect() - } - } - } - merlin.registerConnectable { - if (!hasInternetAccess) { - Timber.v("On Connect") - hasInternetAccess = true - val localListeners = listeners.toList() - localListeners.forEach { - it.onConnect() - } - } - } - } + /** + * Wait until we get internet connection. + */ + suspend fun waitUntilConnected() - override fun onMoveToBackground() { - 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) - } - }) - } - } - } - - fun register(listener: Listener) { - listeners.add(listener) - } - - fun unregister(listener: Listener) { - listeners.remove(listener) - } + fun register(listener: Listener) + fun unregister(listener: Listener) interface Listener { fun onConnect() { @@ -125,3 +57,138 @@ internal class NetworkConnectivityChecker @Inject constructor(private val contex } } } + +@SessionScope +internal class MerlinNetworkConnectivityChecker @Inject constructor(context: Context, + homeServerConnectionConfig: HomeServerConnectionConfig, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val backgroundDetectionObserver: BackgroundDetectionObserver) + : NetworkConnectivityChecker { + + private val waitingForNetwork = AtomicBoolean(false) + private val isMerlinBounded = AtomicBoolean(false) + private val endpointString = "${homeServerConnectionConfig.homeServerUri}/_matrix/client/versions" + private val endpoint = Endpoint.from(endpointString) + private val responseCodeValidator = ResponseCodeValidator { responseCode -> + responseCode == 204 || responseCode == 400 || responseCode == 404 + } + + private val merlin = Merlin.Builder() + .withEndpoint(endpoint) + .withResponseCodeValidator(responseCodeValidator) + .withAllCallbacks() + .build(context) + + private val merlinsBeard = MerlinsBeard.Builder() + .withEndpoint(endpoint) + .withResponseCodeValidator(responseCodeValidator) + .build(context) + + private val hasInternetAccess = AtomicBoolean(merlinsBeard.isConnected) + + private val listeners = Collections.synchronizedSet(LinkedHashSet()) + + private val backgroundDetectionObserverListener = object : BackgroundDetectionObserver.Listener { + override fun onMoveToForeground() { + bindMerlinIfNeeded() + } + + override fun onMoveToBackground() { + unbindMerlinIfNeeded() + } + } + + /** + * Returns true when internet is available + */ + @WorkerThread + override fun hasInternetAccess(forcePing: Boolean): Boolean { + return if (forcePing) { + merlinsBeard.hasInternetAccess() + } else { + hasInternetAccess.get() + } + } + + private fun bindMerlinIfNeeded() { + if (isMerlinBounded.get()) { + return + } + Timber.v("Bind merlin") + isMerlinBounded.set(true) + merlin.bind() + merlinsBeard.hasInternetAccess { + hasInternetAccess.set(it) + } + merlin.registerBindable { + Timber.v("On Network available: ${it.isAvailable}") + } + merlin.registerDisconnectable { + Timber.v("On Disconnect") + hasInternetAccess.set(false) + val localListeners = listeners.toList() + localListeners.forEach { + it.onDisconnect() + } + } + merlin.registerConnectable { + Timber.v("On Connect") + hasInternetAccess.set(true) + val localListeners = listeners.toList() + localListeners.forEach { + it.onConnect() + } + } + } + + private fun unbindMerlinIfNeeded() { + if (backgroundDetectionObserver.isInBackground && !waitingForNetwork.get() && isMerlinBounded.get()) { + isMerlinBounded.set(false) + Timber.v("Unbind merlin") + merlin.unbind() + } + } + + override suspend fun waitUntilConnected() { + val hasInternetAccess = withContext(coroutineDispatchers.io) { + merlinsBeard.hasInternetAccess() + } + if (hasInternetAccess) { + return + } else { + waitingForNetwork.set(true) + bindMerlinIfNeeded() + Timber.v("Waiting for network...") + suspendCoroutine { continuation -> + register(object : NetworkConnectivityChecker.Listener { + override fun onConnect() { + unregister(this) + waitingForNetwork.set(false) + unbindMerlinIfNeeded() + Timber.v("Connected to network...") + continuation.resume(Unit) + } + }) + } + } + } + + override fun register(listener: NetworkConnectivityChecker.Listener) { + if (listeners.isEmpty()) { + if (backgroundDetectionObserver.isInBackground) { + unbindMerlinIfNeeded() + } else { + bindMerlinIfNeeded() + } + backgroundDetectionObserver.register(backgroundDetectionObserverListener) + } + listeners.add(listener) + } + + override fun unregister(listener: NetworkConnectivityChecker.Listener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + backgroundDetectionObserver.unregister(backgroundDetectionObserverListener) + } + } +} 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 7739405aef..1e22df7ab3 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 @@ -35,6 +35,8 @@ import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.* import im.vector.matrix.android.internal.network.AccessTokenInterceptor +import im.vector.matrix.android.internal.network.MerlinNetworkConnectivityChecker +import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater @@ -175,6 +177,9 @@ internal abstract class SessionModule { @Binds abstract fun bindSession(session: DefaultSession): Session + @Binds + abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: MerlinNetworkConnectivityChecker): NetworkConnectivityChecker + @Binds @IntoSet abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt index aa7b4321dc..32f7388d74 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.timeline +import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.filter.FilterRepository import im.vector.matrix.android.internal.session.room.RoomAPI @@ -37,10 +38,12 @@ internal class DefaultPaginationTask @Inject constructor( private val roomAPI: RoomAPI, private val filterRepository: FilterRepository, private val tokenChunkEventPersistor: TokenChunkEventPersistor, - private val eventBus: EventBus + private val eventBus: EventBus, + private val networkConnectivityChecker: NetworkConnectivityChecker ) : PaginationTask { override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { + networkConnectivityChecker.waitUntilConnected() val filter = filterRepository.getRoomFilter() val chunk = executeRequest(eventBus) { apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) 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 3ca4062161..573a46f10a 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 @@ -38,7 +38,6 @@ 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 import im.vector.matrix.android.internal.database.query.whereInRoom -import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer @@ -504,7 +503,6 @@ internal class DefaultTimeline( Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask .configureWith(params) { - this.constraints = TaskConstraints(connectedToNetwork = true) this.callback = object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { when (data) { 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 df050d7f30..c6845ad0c3 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 @@ -28,7 +28,10 @@ import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -97,14 +100,6 @@ abstract class SyncService : Service() { } private suspend fun doSync() { - if (!networkConnectivityChecker.hasInternetAccess()) { - Timber.v("No network reschedule to avoid wasting resources") - sessionId?.also { - onRescheduleAsked(it, isInitialSync, delay = 10_000L) - } - stopMe() - return - } Timber.v("Execute sync request with timeout 0") val params = SyncTask.Params(TIME_OUT) try { @@ -120,9 +115,11 @@ abstract class SyncService : Service() { if (throwable.isTokenError()) { stopMe() } else { - Timber.v("Retry to sync in 5s") - delay(DELAY_FAILURE) - doSync() + Timber.v("Should be rescheduled to avoid wasting resources") + sessionId?.also { + onRescheduleAsked(it, isInitialSync, delay = 10_000L) + } + stopMe() } } } @@ -165,6 +162,5 @@ abstract class SyncService : Service() { companion object { const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID" 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 c550d40e1e..878d7ca5dc 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 @@ -25,7 +25,12 @@ 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.util.BackgroundDetectionObserver -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import timber.log.Timber import java.net.SocketTimeoutException import javax.inject.Inject @@ -98,16 +103,16 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, backgroundDetectionObserver.register(this) while (state != SyncState.Killing) { Timber.v("Entering loop, state: $state") - if (!networkConnectivityChecker.hasInternetAccess()) { - Timber.v("No network. Waiting...") - updateStateTo(SyncState.NoNetwork) - synchronized(lock) { lock.wait() } - Timber.v("...unlocked") - } else if (!isStarted) { + if (!isStarted) { Timber.v("Sync is Paused. Waiting...") updateStateTo(SyncState.Paused) synchronized(lock) { lock.wait() } Timber.v("...unlocked") + } else if (!networkConnectivityChecker.hasInternetAccess(forcePing = false)) { + Timber.v("No network. Waiting...") + updateStateTo(SyncState.NoNetwork) + synchronized(lock) { lock.wait() } + Timber.v("...unlocked") } else if (!isTokenValid) { Timber.v("Token is invalid. Waiting...") updateStateTo(SyncState.InvalidToken) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt index 1bf939b91b..b87d2df191 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt @@ -36,7 +36,6 @@ internal data class ConfigurableTask( val id: UUID, val callbackThread: TaskThread, val executionThread: TaskThread, - val constraints: TaskConstraints, val retryCount: Int, val callback: MatrixCallback @@ -48,7 +47,6 @@ internal data class ConfigurableTask( var id: UUID = UUID.randomUUID(), var callbackThread: TaskThread = TaskThread.MAIN, var executionThread: TaskThread = TaskThread.IO, - var constraints: TaskConstraints = TaskConstraints(), var retryCount: Int = 0, var callback: MatrixCallback = object : MatrixCallback {} ) { @@ -59,7 +57,6 @@ internal data class ConfigurableTask( id = id, callbackThread = callbackThread, executionThread = executionThread, - constraints = constraints, retryCount = retryCount, callback = callback ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt deleted file mode 100644 index d259576a11..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - - */ -package im.vector.matrix.android.internal.task - -data class TaskConstraints( - val connectedToNetwork: Boolean = false -) 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 244cc83901..8dcf85f707 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 @@ -22,14 +22,18 @@ import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.toCancelable -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.EmptyCoroutineContext @MatrixScope -internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val networkConnectivityChecker: NetworkConnectivityChecker) { +internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers) { private val executorScope = CoroutineScope(SupervisorJob()) @@ -40,10 +44,6 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers withContext(task.executionThread.toDispatcher()) { Timber.v("Enqueue task $task") retry(task.retryCount) { - if (task.constraints.connectedToNetwork) { - Timber.v("Waiting network for $task") - networkConnectivityChecker.waitUntilConnected() - } Timber.v("Execute task $task on ${Thread.currentThread().name}") task.execute(task.params) } 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 index 314e12db05..7ca96c9c13 100644 --- a/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt +++ b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt @@ -15,15 +15,13 @@ */ 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.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.riotx.features.notifications.NotificationUtils class VectorSyncService : SyncService() { @@ -69,17 +67,6 @@ class VectorSyncService : SyncService() { } private fun reschedule(sessionId: String, delay: Long) { - val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - PendingIntent.getForegroundService(this, 0, newIntent(this, sessionId), 0) - } else { - PendingIntent.getService(this, 0, newIntent(this, sessionId), 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) - } + AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext, sessionId, delay) } } From 76065ac4fc981708942a73088c2ad8be9ad58361 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Jan 2020 14:43:39 +0100 Subject: [PATCH 04/34] Read: allow setting read marker and read receipt to latest known event independently --- .../api/session/room/read/ReadService.kt | 8 ++- .../session/room/create/CreateRoomTask.kt | 2 +- .../room/membership/joining/JoinRoomTask.kt | 2 +- .../session/room/read/DefaultReadService.kt | 19 ++++++- .../session/room/read/MarkAllRoomsReadTask.kt | 2 +- .../session/room/read/SetReadMarkersTask.kt | 57 +++++++++++-------- .../home/room/detail/RoomDetailViewModel.kt | 4 +- .../NotificationBroadcastReceiver.kt | 3 +- 8 files changed, 64 insertions(+), 33 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt index c9bb6fbf9b..a7995835c5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -26,10 +26,16 @@ import im.vector.matrix.android.api.util.Optional */ interface ReadService { + enum class MarkAsReadParams{ + READ_RECEIPT, + READ_MARKER, + BOTH + } + /** * Force the read marker to be set on the latest event. */ - fun markAllAsRead(callback: MatrixCallback) + fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, callback: MatrixCallback) /** * Set the read receipt on the event with provided eventId. 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 6567b7ad97..b4f7bdcd55 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 @@ -88,7 +88,7 @@ internal class DefaultCreateRoomTask @Inject constructor( } private suspend fun setReadMarkers(roomId: String) { - val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true) return readMarkersTask.execute(setReadMarkerParams) } } 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 d4341951eb..0153930226 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 @@ -64,7 +64,7 @@ internal class DefaultJoinRoomTask @Inject constructor( } private suspend fun setReadMarkers(roomId: String) { - val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true) readMarkersTask.execute(setReadMarkerParams) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index a9a0f60083..36ad2f168f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -50,10 +50,14 @@ internal class DefaultReadService @AssistedInject constructor( fun create(roomId: String): ReadService } - override fun markAllAsRead(callback: MatrixCallback) { - val params = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + override fun markAsRead(params: ReadService.MarkAsReadParams, callback: MatrixCallback) { + val taskParams = SetReadMarkersTask.Params( + roomId = roomId, + forceReadMarker = params.forceReadMarker(), + forceReadReceipt = params.forceReadReceipt() + ) setReadMarkersTask - .configureWith(params) { + .configureWith(taskParams) { this.callback = callback } .executeBy(taskExecutor) @@ -110,4 +114,13 @@ internal class DefaultReadService @AssistedInject constructor( it.firstOrNull() ?: emptyList() } } + + private fun ReadService.MarkAsReadParams.forceReadMarker(): Boolean { + return this == ReadService.MarkAsReadParams.READ_MARKER || this == ReadService.MarkAsReadParams.BOTH + } + + private fun ReadService.MarkAsReadParams.forceReadReceipt(): Boolean { + return this == ReadService.MarkAsReadParams.READ_RECEIPT || this == ReadService.MarkAsReadParams.BOTH + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt index 99376a981a..b7b63dcdf9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt @@ -29,7 +29,7 @@ internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readM override suspend fun execute(params: MarkAllRoomsReadTask.Params) { params.roomIds.forEach { roomId -> - readMarkersTask.execute(SetReadMarkersTask.Params(roomId, markAllAsRead = true)) + readMarkersTask.execute(SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true)) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 6a0d04193d..68a5e30a3f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.database.query.isReadMarkerMoreRecent import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.sync.ReadReceiptHandler @@ -35,16 +36,16 @@ import io.realm.Realm import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject -import kotlin.collections.HashMap import kotlin.collections.set internal interface SetReadMarkersTask : Task { data class Params( val roomId: String, - val markAllAsRead: Boolean = false, val fullyReadEventId: String? = null, - val readReceiptEventId: String? = null + val readReceiptEventId: String? = null, + val forceReadReceipt: Boolean = false, + val forceReadMarker: Boolean = false ) } @@ -57,22 +58,24 @@ internal class DefaultSetReadMarkersTask @Inject constructor( private val roomFullyReadHandler: RoomFullyReadHandler, private val readReceiptHandler: ReadReceiptHandler, @UserId private val userId: String, - private val eventBus: EventBus + private val eventBus: EventBus, + private val networkConnectivityChecker: NetworkConnectivityChecker ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { val markers = HashMap() - Timber.v("Execute set read marker with params: $params") - val (fullyReadEventId, readReceiptEventId) = if (params.markAllAsRead) { - val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> - TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId - } - Pair(latestSyncedEventId, latestSyncedEventId) - } else { - Pair(params.fullyReadEventId, params.readReceiptEventId) + val latestSyncedEventId = latestSyncedEventId(params.roomId) + val fullyReadEventId = if(params.forceReadMarker){ + latestSyncedEventId + }else { + params.fullyReadEventId + } + val readReceiptEventId = if(params.forceReadReceipt){ + latestSyncedEventId + }else { + params.readReceiptEventId } - if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) { if (LocalEcho.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event $fullyReadEventId") @@ -80,7 +83,6 @@ internal class DefaultSetReadMarkersTask @Inject constructor( markers[READ_MARKER] = fullyReadEventId } } - if (readReceiptEventId != null && !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) { if (LocalEcho.isLocalEchoId(readReceiptEventId)) { @@ -89,16 +91,24 @@ internal class DefaultSetReadMarkersTask @Inject constructor( markers[READ_RECEIPT] = readReceiptEventId } } + + val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId + updateDatabase(params.roomId, markers, shouldUpdateRoomSummary) if (markers.isEmpty()) { return } - updateDatabase(params.roomId, markers) + networkConnectivityChecker.waitUntilConnected() executeRequest(eventBus) { apiCall = roomAPI.sendReadMarker(params.roomId, markers) } } - private suspend fun updateDatabase(roomId: String, markers: HashMap) { + private fun latestSyncedEventId(roomId: String): String? = + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId + } + + private suspend fun updateDatabase(roomId: String, markers: HashMap, shouldUpdateRoomSummary: Boolean) { monarchy.awaitTransaction { realm -> val readMarkerId = markers[READ_MARKER] val readReceiptId = markers[READ_RECEIPT] @@ -108,14 +118,13 @@ internal class DefaultSetReadMarkersTask @Inject constructor( if (readReceiptId != null) { val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId) readReceiptHandler.handle(realm, roomId, readReceiptContent, false) - val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId - if (isLatestReceived) { - val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@awaitTransaction - roomSummary.notificationCount = 0 - roomSummary.highlightCount = 0 - roomSummary.hasUnreadMessages = false - } + } + if(shouldUpdateRoomSummary){ + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: return@awaitTransaction + roomSummary.notificationCount = 0 + roomSummary.highlightCount = 0 + roomSummary.hasUnreadMessages = false } } } 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 db938c14e5..7d48124c5a 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 @@ -41,6 +41,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent +import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -149,6 +150,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeUnreadState() room.getRoomSummaryLive() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() + room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, object : MatrixCallback {}) // Inform the SDK that the room is displayed session.onRoomDisplayed(initialState.roomId) } @@ -753,7 +755,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleMarkAllAsRead() { - room.markAllAsRead(object : MatrixCallback {}) + room.markAsRead(ReadService.MarkAsReadParams.BOTH, object : MatrixCallback {}) } private fun handleReportContent(action: RoomDetailAction.ReportContent) { diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt index c9dc131b42..816e0dc0ad 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt @@ -23,6 +23,7 @@ import androidx.core.app.RemoteInput import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.vectorComponent @@ -88,7 +89,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { private fun handleMarkAsRead(roomId: String) { activeSessionHolder.getActiveSession().let { session -> session.getRoom(roomId) - ?.markAllAsRead(object : MatrixCallback {}) + ?.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, object : MatrixCallback {}) } } From fee2ec6b6690a98e5b7cc32959fce806fad3061f Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 20 Jan 2020 21:13:53 +0100 Subject: [PATCH 05/34] Scroll when event build come from sync/send + remove use of monarchy writeAsync --- .../api/session/room/send/DraftService.kt | 6 +- .../api/session/room/timeline/Timeline.kt | 5 + .../database/helper/RoomEntityHelper.kt | 5 +- .../database/mapper/TimelineEventMapper.kt | 9 +- .../session/group/DefaultGetGroupDataTask.kt | 5 +- .../internal/session/room/RoomFactory.kt | 30 ++-- .../session/room/draft/DefaultDraftService.kt | 134 ++------------- .../session/room/draft/DraftRepository.kt | 156 ++++++++++++++++++ .../room/relation/DefaultRelationService.kt | 2 +- .../session/room/send/DefaultSendService.kt | 103 ++++-------- .../room/send/LocalEchoEventFactory.kt | 44 +++-- .../session/room/send/LocalEchoRepository.kt | 147 +++++++++++++++++ .../session/room/timeline/DefaultTimeline.kt | 31 +++- .../room/timeline/DefaultTimelineService.kt | 26 +-- .../internal/session/sync/RoomSyncHandler.kt | 15 +- .../android/internal/task/TaskExecutor.kt | 2 +- .../riotx/core/utils/NoOpMatrixCallback.kt | 21 +++ .../home/room/detail/RoomDetailFragment.kt | 1 + .../home/room/detail/RoomDetailViewEvents.kt | 1 + .../home/room/detail/RoomDetailViewModel.kt | 45 +++-- .../room/detail/ScrollOnNewMessageCallback.kt | 18 +- .../timeline/TimelineEventController.kt | 7 +- 22 files changed, 538 insertions(+), 275 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt index 2324f1e221..ffb15f4632 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt @@ -17,18 +17,20 @@ package im.vector.matrix.android.api.session.room.send import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable interface DraftService { /** * Save or update a draft to the room */ - fun saveDraft(draft: UserDraft) + fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable /** * Delete the last draft, basically just after sending the message */ - fun deleteDraft() + fun deleteDraft(callback: MatrixCallback): Cancelable /** * Return the current drafts if any, as a live data 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 2280803e5c..16bf522c59 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 @@ -112,6 +112,11 @@ interface Timeline { * Called whenever an error we can't recover from occurred */ fun onTimelineFailure(throwable: Throwable) + + /** + * Call when new events come through the sync + */ + fun onNewTimelineEvents(eventIds: List) } /** 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 b6c6c6c1ac..9d48d477bf 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 @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.fastContains import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper +import io.realm.Realm internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { chunks.remove(chunkEntity) @@ -53,8 +54,7 @@ internal fun RoomEntity.addStateEvent(stateEvent: Event, untimelinedStateEvents.add(entity) } } -internal fun RoomEntity.addSendingEvent(event: Event) { - assertIsManaged() +internal fun RoomEntity.addSendingEvent(realm: Realm, event: Event) { val senderId = event.senderId ?: return val eventEntity = event.toEntity(roomId).apply { this.sendState = SendState.UNSENT @@ -72,3 +72,4 @@ internal fun RoomEntity.addSendingEvent(event: Event) { } sendingTimelineEvents.add(0, timelineEventEntity) } + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 9959f940b6..146b5c3ae4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -43,9 +43,12 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, senderAvatar = timelineEventEntity.senderAvatar, - readReceipts = readReceipts?.sortedByDescending { - it.originServerTs - } ?: emptyList() + readReceipts = readReceipts + ?.distinctBy { + it.user + }?.sortedByDescending { + it.originServerTs + } ?: emptyList() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt index 069c9b8d21..a24eecd251 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.session.group.model.GroupRooms import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse import im.vector.matrix.android.internal.session.group.model.GroupUsers import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction import org.greenrobot.eventbus.EventBus import javax.inject.Inject @@ -53,12 +54,12 @@ internal class DefaultGetGroupDataTask @Inject constructor( insertInDb(groupSummary, groupRooms, groupUsers, groupId) } - private fun insertInDb(groupSummary: GroupSummaryResponse, + private suspend fun insertInDb(groupSummary: GroupSummaryResponse, groupRooms: GroupRooms, groupUsers: GroupUsers, groupId: String) { monarchy - .writeAsync { realm -> + .awaitTransaction { realm -> val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst() ?: realm.createObject(GroupSummaryEntity::class.java, groupId) 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 b24bb73d56..cda43718dd 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 @@ -50,25 +50,25 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val typingServiceFactory: DefaultTypingService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory, - private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory) : + private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory): RoomFactory { override fun create(roomId: String): Room { return DefaultRoom( - roomId, - monarchy, - roomSummaryMapper, - timelineServiceFactory.create(roomId), - sendServiceFactory.create(roomId), - draftServiceFactory.create(roomId), - stateServiceFactory.create(roomId), - reportingServiceFactory.create(roomId), - readServiceFactory.create(roomId), - typingServiceFactory.create(roomId), - cryptoService, - relationServiceFactory.create(roomId), - membershipServiceFactory.create(roomId), - roomPushRuleServiceFactory.create(roomId) + roomId = roomId, + monarchy = monarchy, + roomSummaryMapper = roomSummaryMapper, + timelineService = timelineServiceFactory.create(roomId), + sendService = sendServiceFactory.create(roomId), + draftService = draftServiceFactory.create(roomId), + stateService = stateServiceFactory.create(roomId), + reportingService = reportingServiceFactory.create(roomId), + readService = readServiceFactory.create(roomId), + typingService = typingServiceFactory.create(roomId), + cryptoService = cryptoService, + relationService = relationServiceFactory.create(roomId), + roomMembersService = membershipServiceFactory.create(roomId), + roomPushRuleService = roomPushRuleServiceFactory.create(roomId) ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt index 9fd9cf7c9d..99ec5ede4d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt @@ -17,23 +17,21 @@ package im.vector.matrix.android.internal.session.room.draft import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.UserDraft -import im.vector.matrix.android.internal.database.mapper.DraftMapper -import im.vector.matrix.android.internal.database.model.DraftEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.model.UserDraftsEntity -import im.vector.matrix.android.internal.database.query.where -import io.realm.kotlin.createObject -import timber.log.Timber +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, - private val monarchy: Monarchy + private val draftRepository: DraftRepository, + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers ) : DraftService { @AssistedInject.Factory @@ -45,121 +43,19 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft, * or even move an existing draft to the top of the list */ - override fun saveDraft(draft: UserDraft) { - Timber.d("Draft: saveDraft ${privacySafe(draft)}") - - monarchy.writeAsync { realm -> - - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) - - val userDraftsEntity = roomSummaryEntity.userDrafts - ?: realm.createObject().also { - roomSummaryEntity.userDrafts = it - } - - userDraftsEntity.let { userDraftEntity -> - // Save only valid draft - if (draft.isValid()) { - // Add a new draft or update the current one? - val newDraft = DraftMapper.map(draft) - - // Is it an update of the top draft? - val topDraft = userDraftEntity.userDrafts.lastOrNull() - - if (topDraft == null) { - Timber.d("Draft: create a new draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) { - // top draft is an edit - if (newDraft.draftMode == DraftEntity.MODE_EDIT) { - if (topDraft.linkedEventId == newDraft.linkedEventId) { - // Update the top draft - Timber.d("Draft: update the top edit draft ${privacySafe(draft)}") - topDraft.content = newDraft.content - } else { - // Check a previously EDIT draft with the same id - val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find { - it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId - } - - if (existingEditDraftOfSameEvent != null) { - // Ignore the new text, restore what was typed before, by putting the draft to the top - Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent) - userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent) - } else { - Timber.d("Draft: add a new edit draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } - } - } else { - // Add a new regular draft to the top - Timber.d("Draft: add a new draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } - } else { - // Top draft is not an edit - if (newDraft.draftMode == DraftEntity.MODE_EDIT) { - Timber.d("Draft: create a new edit draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } else { - // Update the top draft - Timber.d("Draft: update the top draft ${privacySafe(draft)}") - topDraft.draftMode = newDraft.draftMode - topDraft.content = newDraft.content - topDraft.linkedEventId = newDraft.linkedEventId - } - } - } else { - // There is no draft to save, so the composer was clear - Timber.d("Draft: delete a draft") - - val topDraft = userDraftEntity.userDrafts.lastOrNull() - - if (topDraft == null) { - Timber.d("Draft: nothing to do") - } else { - // Remove the top draft - Timber.d("Draft: remove the top draft") - userDraftEntity.userDrafts.remove(topDraft) - } - } - } + override fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + draftRepository.saveDraft(roomId, draft) } } - private fun privacySafe(o: Any): Any { - if (BuildConfig.LOG_PRIVATE_DATA) { - return o - } - - return "" - } - - override fun deleteDraft() { - Timber.d("Draft: deleteDraft()") - - monarchy.writeAsync { realm -> - UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> - if (userDraftsEntity.userDrafts.isNotEmpty()) { - userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) - } - } + override fun deleteDraft(callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + draftRepository.deleteDraft(roomId) } } override fun getDraftsLive(): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { UserDraftsEntity.where(it, roomId) }, - { - it.userDrafts.map { draft -> - DraftMapper.map(draft) - } - } - ) - return Transformations.map(liveData) { - it.firstOrNull() ?: emptyList() - } + return draftRepository.getDraftsLive(roomId) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt new file mode 100644 index 0000000000..b00bf2aadb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt @@ -0,0 +1,156 @@ +/* + * 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.session.room.draft + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.api.session.room.send.UserDraft +import im.vector.matrix.android.internal.database.mapper.DraftMapper +import im.vector.matrix.android.internal.database.model.DraftEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.model.UserDraftsEntity +import im.vector.matrix.android.internal.database.query.where +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 + +class DraftRepository @Inject constructor(private val monarchy: Monarchy) { + + suspend fun saveDraft(roomId: String, userDraft: UserDraft) { + monarchy.awaitTransaction { + saveDraft(it, userDraft, roomId) + } + } + + suspend fun deleteDraft(roomId: String) { + monarchy.awaitTransaction { + deleteDraft(it, roomId) + } + } + + private fun deleteDraft(realm: Realm, roomId: String) { + UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> + if (userDraftsEntity.userDrafts.isNotEmpty()) { + userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) + } + } + } + + private fun saveDraft(realm: Realm, draft: UserDraft, roomId: String) { + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) + + val userDraftsEntity = roomSummaryEntity.userDrafts + ?: realm.createObject().also { + roomSummaryEntity.userDrafts = it + } + + userDraftsEntity.let { userDraftEntity -> + // Save only valid draft + if (draft.isValid()) { + // Add a new draft or update the current one? + val newDraft = DraftMapper.map(draft) + + // Is it an update of the top draft? + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: create a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) { + // top draft is an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + if (topDraft.linkedEventId == newDraft.linkedEventId) { + // Update the top draft + Timber.d("Draft: update the top edit draft ${privacySafe(draft)}") + topDraft.content = newDraft.content + } else { + // Check a previously EDIT draft with the same id + val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find { + it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId + } + + if (existingEditDraftOfSameEvent != null) { + // Ignore the new text, restore what was typed before, by putting the draft to the top + Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent) + userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent) + } else { + Timber.d("Draft: add a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } + } else { + // Add a new regular draft to the top + Timber.d("Draft: add a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } else { + // Top draft is not an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + Timber.d("Draft: create a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else { + // Update the top draft + Timber.d("Draft: update the top draft ${privacySafe(draft)}") + topDraft.draftMode = newDraft.draftMode + topDraft.content = newDraft.content + topDraft.linkedEventId = newDraft.linkedEventId + } + } + } else { + // There is no draft to save, so the composer was clear + Timber.d("Draft: delete a draft") + + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: nothing to do") + } else { + // Remove the top draft + Timber.d("Draft: remove the top draft") + userDraftEntity.userDrafts.remove(topDraft) + } + } + } + } + + fun getDraftsLive(roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { UserDraftsEntity.where(it, roomId) }, + { + it.userDrafts.map { draft -> + DraftMapper.map(draft) + } + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: emptyList() + } + } + + private fun privacySafe(o: Any): Any { + if (BuildConfig.LOG_PRIVATE_DATA) { + return o + } + return "" + } +} 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 1b2b27a3eb..867c9eab1f 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 @@ -242,6 +242,6 @@ internal class DefaultRelationService @AssistedInject constructor( * the same transaction id is received (in unsigned data) */ private fun saveLocalEcho(event: Event) { - eventFactory.saveLocalEcho(monarchy, event) + eventFactory.createLocalEcho(event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 507c8dd247..5061b5f2c4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -17,35 +17,35 @@ package im.vector.matrix.android.internal.session.room.send import android.content.Context -import androidx.work.* +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.Operation +import androidx.work.WorkManager import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.isImageMessage +import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.room.send.SendService 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.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag -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.RoomEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates -import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker 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.startChain +import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -59,7 +59,9 @@ internal class DefaultSendService @AssistedInject constructor( @SessionId private val sessionId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, - private val monarchy: Monarchy + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val localEchoRepository: LocalEchoRepository ) : SendService { @AssistedInject.Factory @@ -71,15 +73,14 @@ internal class DefaultSendService @AssistedInject constructor( override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { - saveLocalEcho(it) + createLocalEcho(it) } - return sendEvent(event) } override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { - saveLocalEcho(it) + createLocalEcho(it) } return sendEvent(event) @@ -157,13 +158,8 @@ internal class DefaultSendService @AssistedInject constructor( } override fun deleteFailedEcho(localEcho: TimelineEvent) { - monarchy.writeAsync { realm -> - TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.let { - it.deleteFromRealm() - } - EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { - it.deleteFromRealm() - } + taskExecutor.executorScope.launch { + localEchoRepository.deleteFailedEcho(roomId, localEcho) } } @@ -181,67 +177,26 @@ internal class DefaultSendService @AssistedInject constructor( .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) .enqueue() } - - monarchy.writeAsync { realm -> - RoomEntity.where(realm, roomId).findFirst()?.let { room -> - room.sendingTimelineEvents.forEach { - it.root?.sendState = SendState.UNDELIVERED - } - } + taskExecutor.executorScope.launch { + localEchoRepository.clearSendingQueue(roomId) } } override fun resendAllFailedMessages() { - monarchy.writeAsync { realm -> - TimelineEventEntity - .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) - .sortedBy { it.root?.originServerTs ?: 0 } - .forEach { timelineEventEntity -> - timelineEventEntity.root?.let { - val event = it.asDomain() - when (event.getClearType()) { - EventType.MESSAGE, - EventType.REDACTION, - EventType.REACTION -> { - val content = event.getClearContent().toModel() - if (content != null) { - when (content.type) { - MessageType.MSGTYPE_EMOTE, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_LOCATION, - MessageType.MSGTYPE_TEXT -> { - it.sendState = SendState.UNSENT - sendEvent(event) - } - MessageType.MSGTYPE_FILE, - MessageType.MSGTYPE_VIDEO, - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_AUDIO -> { - // need to resend the attachement - } - else -> { - Timber.e("Cannot resend message ${event.type} / ${content.type}") - } - } - } else { - Timber.e("Unsupported message to resend ${event.type}") - } - } - else -> { - Timber.e("Unsupported message to resend ${event.type}") - } - } - } - } + taskExecutor.executorScope.launch { + val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) + eventsToResend.forEach { + sendEvent(it) + } + localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) } } override fun sendMedia(attachment: ContentAttachmentData): Cancelable { // Create an event with the media file path val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { - saveLocalEcho(it) + createLocalEcho(it) } - return internalSendMedia(event, attachment) } @@ -276,8 +231,8 @@ internal class DefaultSendService @AssistedInject constructor( return CancelableWork(context, sendWork.id) } - private fun saveLocalEcho(event: Event) { - localEchoEventFactory.saveLocalEcho(monarchy, event) + private fun createLocalEcho(event: Event) { + localEchoEventFactory.createLocalEcho(event) } private fun buildWorkName(identifier: String): String { @@ -306,7 +261,7 @@ internal class DefaultSendService @AssistedInject constructor( private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { - saveLocalEcho(it) + createLocalEcho(it) } val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 7a935783cf..383c961aaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -18,26 +18,42 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever import androidx.exifinterface.media.ExifInterface -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.content.ContentAttachmentData -import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.message.* +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.RelationType +import im.vector.matrix.android.api.session.events.model.UnsignedData +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.AudioInfo +import im.vector.matrix.android.api.session.room.model.message.FileInfo +import im.vector.matrix.android.api.session.room.model.message.ImageInfo +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +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.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo +import im.vector.matrix.android.api.session.room.model.message.VideoInfo +import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent 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.internal.database.helper.addSendingEvent -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.content.ThumbnailExtractor -import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.StringProvider +import kotlinx.coroutines.launch import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import javax.inject.Inject @@ -54,8 +70,9 @@ import javax.inject.Inject internal class LocalEchoEventFactory @Inject constructor( @UserId private val userId: String, private val stringProvider: StringProvider, - private val roomSummaryUpdater: RoomSummaryUpdater, - private val textPillsUtils: TextPillsUtils + private val textPillsUtils: TextPillsUtils, + private val taskExecutor: TaskExecutor, + private val localEchoRepository: LocalEchoRepository ) { // TODO Inject private val parser = Parser.builder().build() @@ -402,13 +419,10 @@ internal class LocalEchoEventFactory @Inject constructor( ) } - fun saveLocalEcho(monarchy: Monarchy, event: Event) { + fun createLocalEcho(event: Event){ checkNotNull(event.roomId) { "Your event should have a roomId" } - monarchy.writeAsync { realm -> - val roomEntity = RoomEntity.where(realm, roomId = event.roomId).findFirst() - ?: return@writeAsync - roomEntity.addSendingEvent(event) - roomSummaryUpdater.update(realm, event.roomId) + taskExecutor.executorScope.launch { + localEchoRepository.createLocalEcho(event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt new file mode 100644 index 0000000000..5cdf4d1d4f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -0,0 +1,147 @@ +/* + * 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.session.room.send + +import com.zhuinden.monarchy.Monarchy +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.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.database.helper.nextId +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.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.query.findAllInRoomWithSendStates +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper +import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline +import im.vector.matrix.android.internal.util.awaitTransaction +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val eventBus: EventBus) { + + suspend fun createLocalEcho(event: Event) { + val roomId = event.roomId ?: return + val senderId = event.senderId ?: return + val eventId = event.eventId ?: return + eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = listOf(eventId))) + monarchy.awaitTransaction { realm -> + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction + val eventEntity = event.toEntity(roomId).apply { + this.sendState = SendState.UNSENT + } + val roomMemberHelper = RoomMemberHelper(realm, roomId) + val myUser = roomMemberHelper.getLastRoomMember(senderId) + val localId = TimelineEventEntity.nextId(realm) + val timelineEventEntity = TimelineEventEntity(localId).also { + it.root = eventEntity + it.eventId = event.eventId + it.roomId = roomId + it.senderName = myUser?.displayName + it.senderAvatar = myUser?.avatarUrl + it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName) + } + roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) + roomSummaryUpdater.update(realm, roomId) + } + } + + suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { + monarchy.awaitTransaction { realm -> + TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() + EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { + it.deleteFromRealm() + } + } + } + + suspend fun clearSendingQueue(roomId: String) { + monarchy.awaitTransaction { realm -> + RoomEntity.where(realm, roomId).findFirst()?.let { room -> + room.sendingTimelineEvents.forEach { + it.root?.sendState = SendState.UNDELIVERED + } + } + } + } + + suspend fun updateSendState(roomId: String, eventIds: List, sendState: SendState) { + monarchy.awaitTransaction { realm -> + val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll() + timelineEvents.forEach { + it.root?.sendState = sendState + } + } + } + + fun getAllFailedEventsToResend(roomId: String): List { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + TimelineEventEntity + .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) + .sortedByDescending { it.root?.displayIndex ?: 0 } + .mapNotNull { it.root?.asDomain() } + .filter { event -> + when (event.getClearType()) { + EventType.MESSAGE, + EventType.REDACTION, + EventType.REACTION -> { + val content = event.getClearContent().toModel() + if (content != null) { + when (content.type) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_LOCATION, + MessageType.MSGTYPE_TEXT -> { + true + } + MessageType.MSGTYPE_FILE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO -> { + // need to resend the attachement + false + } + else -> { + Timber.e("Cannot resend message ${event.type} / ${content.type}") + false + } + } + } else { + Timber.e("Unsupported message to resend ${event.type}") + false + } + } + else -> { + Timber.e("Unsupported message to resend ${event.type}") + false + } + } + } + } + } +} 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 573a46f10a..62268349b3 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 @@ -50,6 +50,9 @@ import io.realm.RealmConfiguration import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import timber.log.Timber import java.util.Collections import java.util.UUID @@ -72,9 +75,12 @@ internal class DefaultTimeline( private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, - private val hiddenReadReceipts: TimelineHiddenReadReceipts + private val hiddenReadReceipts: TimelineHiddenReadReceipts, + private val eventBus: EventBus ) : Timeline, TimelineHiddenReadReceipts.Delegate { + data class OnNewTimelineEvents(val roomId: String, val eventIds: List) + companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") } @@ -128,7 +134,7 @@ internal class DefaultTimeline( if (hasChange) postSnapshot() } -// Public methods ****************************************************************************** + // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { BACKGROUND_HANDLER.post { @@ -159,6 +165,7 @@ internal class DefaultTimeline( override fun start() { if (isStarted.compareAndSet(false, true)) { Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") + eventBus.register(this) BACKGROUND_HANDLER.post { eventDecryptor.start() val realm = Realm.getInstance(realmConfiguration) @@ -190,12 +197,13 @@ internal class DefaultTimeline( } private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { - return settings.buildReadReceipts && (settings.filterEdits || settings.filterTypes) + return buildReadReceipts && (filterEdits || filterTypes) } override fun dispose() { if (isStarted.compareAndSet(true, false)) { isReady.set(false) + eventBus.unregister(this) Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") cancelableBag.cancel() BACKGROUND_HANDLER.removeCallbacksAndMessages(null) @@ -316,6 +324,15 @@ internal class DefaultTimeline( postSnapshot() } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) { + if (onNewTimelineEvents.roomId == roomId) { + listeners.forEach { + it.onNewTimelineEvents(onNewTimelineEvents.eventIds) + } + } + } + // Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { @@ -401,14 +418,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() @@ -506,10 +523,10 @@ internal class DefaultTimeline( this.callback = object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { + TokenChunkEventPersistor.Result.SUCCESS -> { Timber.v("Success fetching $limit items $direction from pagination request") } - TokenChunkEventPersistor.Result.REACHED_END -> { + TokenChunkEventPersistor.Result.REACHED_END -> { postSnapshot() } TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index d92dbd66be..5ed3c76ed6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -34,9 +34,11 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.fetchCopyMap +import org.greenrobot.eventbus.EventBus internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, private val monarchy: Monarchy, + private val eventBus: EventBus, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, private val cryptoService: CryptoService, @@ -52,17 +54,19 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv } override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { - return DefaultTimeline(roomId, - eventId, - monarchy.realmConfiguration, - taskExecutor, - contextOfEventTask, - clearUnlinkedEventsTask, - paginationTask, - cryptoService, - timelineEventMapper, - settings, - TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) + return DefaultTimeline( + roomId = roomId, + initialEventId = eventId, + realmConfiguration = monarchy.realmConfiguration, + taskExecutor = taskExecutor, + contextOfEventTask = contextOfEventTask, + clearUnlinkedEventsTask = clearUnlinkedEventsTask, + paginationTask = paginationTask, + cryptoService = cryptoService, + timelineEventMapper = timelineEventMapper, + settings = settings, + hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), + eventBus = eventBus ) } 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 a1699d9f55..9a24eb502a 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 @@ -36,11 +36,13 @@ import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler import im.vector.matrix.android.internal.session.room.read.FullyReadContent +import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.typing.TypingEventContent import im.vector.matrix.android.internal.session.sync.model.* import io.realm.Realm import io.realm.kotlin.createObject +import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject @@ -50,7 +52,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val roomFullyReadHandler: RoomFullyReadHandler, private val cryptoService: DefaultCryptoService, private val roomMemberEventHandler: RoomMemberEventHandler, - private val timelineEventSenderVisitor: TimelineEventSenderVisitor) { + private val timelineEventSenderVisitor: TimelineEventSenderVisitor, + private val eventBus: EventBus) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -130,6 +133,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle if (roomSync.timeline?.events?.isNotEmpty() == true) { val chunkEntity = handleTimelineEvents( realm, + roomId, roomEntity, roomSync.timeline.events, roomSync.timeline.prevToken, @@ -161,7 +165,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { - val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) + val chunkEntity = handleTimelineEvents(realm, roomId, roomEntity, roomSync.inviteState.events) roomEntity.addOrUpdate(chunkEntity) } val hasRoomMember = roomSync.inviteState?.events?.firstOrNull { @@ -183,6 +187,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } private fun handleTimelineEvents(realm: Realm, + roomId: String, roomEntity: RoomEntity, eventList: List, prevToken: String? = null, @@ -202,7 +207,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle chunkEntity.isUnlinked = false val timelineEvents = ArrayList(eventList.size) + val eventIds = ArrayList(eventList.size) for (event in eventList) { + if(event.eventId != null) { + eventIds.add(event.eventId) + } chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also { timelineEvents.add(it) } @@ -221,6 +230,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomMemberEventHandler.handle(realm, roomEntity.roomId, event) } timelineEventSenderVisitor.visit(timelineEvents) + // posting new events to timeline if any is registered + eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = eventIds)) return chunkEntity } 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 8dcf85f707..8dcc15dec6 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 @@ -35,7 +35,7 @@ import kotlin.coroutines.EmptyCoroutineContext @MatrixScope internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers) { - private val executorScope = CoroutineScope(SupervisorJob()) + val executorScope = CoroutineScope(SupervisorJob()) fun execute(task: ConfigurableTask): Cancelable { return executorScope diff --git a/vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt b/vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt new file mode 100644 index 0000000000..f24fc2a09c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt @@ -0,0 +1,21 @@ +/* + * 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.utils + +import im.vector.matrix.android.api.MatrixCallback + +class NoOpMatrixCallback: MatrixCallback 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 27983aa487..45d1ec1c71 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 @@ -312,6 +312,7 @@ class RoomDetailFragment @Inject constructor( .subscribe { when (it) { is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) } } .disposeOnDestroyView() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt index a1ad480584..4de399c838 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt @@ -21,4 +21,5 @@ package im.vector.riotx.features.home.room.detail */ sealed class RoomDetailViewEvents { data class Failure(val throwable: Throwable) : RoomDetailViewEvents() + data class OnNewTimelineEvents(val eventIds: List) : 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 7d48124c5a..0a375ac328 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 @@ -58,6 +63,7 @@ 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.NoOpMatrixCallback import im.vector.riotx.core.utils.PublishDataSource import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser @@ -218,10 +224,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) { withState { when (it.sendMode) { - is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft)) - is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) + is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) } } } @@ -465,7 +471,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun popDraft() { - room.deleteDraft() + room.deleteDraft(object : MatrixCallback {}) } private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { @@ -584,7 +590,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.EDIT(timelineEvent, action.text)) } timelineEvent.root.eventId?.let { - room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: "")) + room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: ""), NoOpMatrixCallback()) } } } @@ -598,9 +604,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Save a new draft and keep the previously entered text, if it was not an edit timelineEvent.root.eventId?.let { if (state.sendMode is SendMode.EDIT) { - room.saveDraft(UserDraft.QUOTE(it, "")) + room.saveDraft(UserDraft.QUOTE(it, ""), NoOpMatrixCallback()) } else { - room.saveDraft(UserDraft.QUOTE(it, action.text)) + room.saveDraft(UserDraft.QUOTE(it, action.text), NoOpMatrixCallback()) } } } @@ -616,9 +622,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Save a new draft and keep the previously entered text, if it was not an edit timelineEvent.root.eventId?.let { if (state.sendMode is SendMode.EDIT) { - room.saveDraft(UserDraft.REPLY(it, "")) + room.saveDraft(UserDraft.REPLY(it, ""), NoOpMatrixCallback()) } else { - room.saveDraft(UserDraft.REPLY(it, action.text)) + room.saveDraft(UserDraft.REPLY(it, action.text), NoOpMatrixCallback()) } } } @@ -630,10 +636,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro withState { if (draft.isNotBlank()) { when (it.sendMode) { - is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft)) - is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft)) - is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft)) - is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft)) + is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft), NoOpMatrixCallback()) + is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback()) + is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback()) + is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback()) } } } @@ -644,10 +650,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro withState { state -> // For edit, just delete the current draft if (state.sendMode is SendMode.EDIT) { - room.deleteDraft() + room.deleteDraft(NoOpMatrixCallback()) } else { // Save a new draft and keep the previously entered text - room.saveDraft(UserDraft.REGULAR(action.text)) + room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback()) } } } @@ -892,6 +898,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro _viewEvents.post(RoomDetailViewEvents.Failure(throwable)) } + override fun onNewTimelineEvents(eventIds: List) { + Timber.v("On new timeline events: $eventIds") + _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds)) + } + override fun onCleared() { timeline.dispose() timeline.removeAllListeners() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt index 2e2945c59e..2180571c3b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -19,15 +19,29 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem import timber.log.Timber class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { + private val newTimelineEventIds = HashSet() + + fun addNewTimelineEventIds(eventIds: List){ + newTimelineEventIds.addAll(eventIds) + } + override fun onInserted(position: Int, count: Int) { Timber.v("On inserted $count count at position: $position") - if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { - layoutManager.scrollToPosition(0) + if(layoutManager.findFirstVisibleItemPosition() != position ){ + return + } + val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return + val firstNewItemIds = firstNewItem.getEventIds() + if(newTimelineEventIds.intersect(firstNewItemIds).isNotEmpty()){ + Timber.v("Should scroll to position: $position") + newTimelineEventIds.clear() + layoutManager.scrollToPosition(position) } } } 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 a08669da3b..1579a77779 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 @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ +import im.vector.riotx.core.epoxy.emptyItem import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.UnreadState @@ -241,6 +242,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // no-op, already handled } + override fun onNewTimelineEvents(eventIds: List) { + // no-op, already handled + } + private fun submitSnapshot(newSnapshot: List) { backgroundHandler.post { inSubmitList = true @@ -346,8 +351,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return positionOfReadMarker } - fun isLoadingForward() = showingForwardLoader - private data class CacheItemData( val localId: Long, val eventId: String?, From c65f25d7ae2a26459c45a1114c49da44b35751c0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Jan 2020 10:18:22 +0100 Subject: [PATCH 06/34] Rx: fix startWith on mainThread --- .../im/vector/matrix/rx/LiveDataObservable.kt | 11 +++++++++- .../main/java/im/vector/matrix/rx/RxRoom.kt | 20 ++++++++++++++----- .../java/im/vector/matrix/rx/RxSession.kt | 16 +++++++++++---- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt index 7958d3efa1..302e775056 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import io.reactivex.Observable import io.reactivex.android.MainThreadDisposable +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers private class LiveDataObservable( @@ -57,6 +58,14 @@ private class LiveDataObservable( } } -fun LiveData.asObservable(): Observable { +internal fun LiveData.asObservable(): Observable { return LiveDataObservable(this).observeOn(Schedulers.computation()) } + +internal fun Observable.startWithCallable(supplier: () -> T): Observable { + val startObservable = Observable + .fromCallable(supplier) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + return startWith(startObservable) +} 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 9491a69ef1..ef55b090b7 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 @@ -35,27 +35,37 @@ class RxRoom(private val room: Room) { fun liveRoomSummary(): Observable> { return room.getRoomSummaryLive().asObservable() - .startWith(room.roomSummary().toOptional()) + .startWithCallable { + room.roomSummary().toOptional() + } } fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable> { return room.getRoomMembersLive(queryParams).asObservable() - .startWith(room.getRoomMembers(queryParams)) + .startWithCallable { + room.getRoomMembers(queryParams) + } } fun liveAnnotationSummary(eventId: String): Observable> { return room.getEventAnnotationsSummaryLive(eventId).asObservable() - .startWith(room.getEventAnnotationsSummary(eventId).toOptional()) + .startWithCallable { + room.getEventAnnotationsSummary(eventId).toOptional() + } } fun liveTimelineEvent(eventId: String): Observable> { return room.getTimeLineEventLive(eventId).asObservable() - .startWith(room.getTimeLineEvent(eventId).toOptional()) + .startWithCallable { + room.getTimeLineEvent(eventId).toOptional() + } } fun liveStateEvent(eventType: String): Observable> { return room.getStateEventLive(eventType).asObservable() - .startWith(room.getStateEvent(eventType).toOptional()) + .startWithCallable { + room.getStateEvent(eventType).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 406e274258..edba4ca9bf 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 @@ -36,17 +36,23 @@ class RxSession(private val session: Session) { fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable> { return session.getRoomSummariesLive(queryParams).asObservable() - .startWith(session.getRoomSummaries(queryParams)) + .startWithCallable { + session.getRoomSummaries(queryParams) + } } fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable> { return session.getGroupSummariesLive(queryParams).asObservable() - .startWith(session.getGroupSummaries(queryParams)) + .startWithCallable { + session.getGroupSummaries(queryParams) + } } fun liveBreadcrumbs(): Observable> { return session.getBreadcrumbsLive().asObservable() - .startWith(session.getBreadcrumbs()) + .startWithCallable { + session.getBreadcrumbs() + } } fun liveSyncState(): Observable { @@ -59,7 +65,9 @@ class RxSession(private val session: Session) { fun liveUser(userId: String): Observable> { return session.getUserLive(userId).asObservable() - .startWith(session.getUser(userId).toOptional()) + .startWithCallable { + session.getUser(userId).toOptional() + } } fun liveUsers(): Observable> { From 4331d2ef4740eed6ec4ef9d3f097c22eabe28bb7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Jan 2020 19:08:55 +0100 Subject: [PATCH 07/34] Network: reword the strategy for handling NetworkConnectivity (remove Merlin) --- matrix-sdk-android/build.gradle | 1 - .../network/NetworkCallbackStrategy.kt | 75 +++++++++ .../network/NetworkConnectivityChecker.kt | 146 ++++-------------- .../internal/network/NetworkInfoReceiver.kt | 35 +++++ .../android/internal/network/Request.kt | 25 ++- .../android/internal/session/SessionModule.kt | 22 ++- .../session/homeserver/CapabilitiesAPI.kt | 6 + .../session/homeserver/HomeserverPinger.kt | 50 ++++++ .../session/room/read/SetReadMarkersTask.kt | 12 +- .../room/timeline/DefaultPaginationTask.kt | 6 +- .../internal/session/sync/job/SyncThread.kt | 36 ++++- .../android/internal/task/ConfigurableTask.kt | 2 - .../android/internal/task/TaskExecutor.kt | 27 +--- .../riotx/core/services/VectorSyncService.kt | 1 + 14 files changed, 277 insertions(+), 167 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkCallbackStrategy.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkInfoReceiver.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeserverPinger.kt diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index e97405125f..b902108680 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -111,7 +111,6 @@ dependencies { implementation 'com.squareup.retrofit2:converter-moshi:2.6.2' implementation 'com.squareup.okhttp3:okhttp:4.2.2' implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' - implementation 'com.novoda:merlin:1.2.0' implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkCallbackStrategy.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkCallbackStrategy.kt new file mode 100644 index 0000000000..6c98838afd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkCallbackStrategy.kt @@ -0,0 +1,75 @@ +/* + * 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.network + +import android.annotation.TargetApi +import android.content.Context +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import javax.inject.Inject + +internal interface NetworkCallbackStrategy { + fun register(hasChanged: () -> Unit) + fun unregister() +} + +internal class FallbackNetworkCallbackStrategy @Inject constructor(private val context: Context, + private val networkInfoReceiver: NetworkInfoReceiver) : NetworkCallbackStrategy { + + @Suppress("DEPRECATION") + val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + + override fun register(hasChanged: () -> Unit) { + networkInfoReceiver.isConnectedCallback = { + hasChanged() + } + context.registerReceiver(networkInfoReceiver, filter) + } + + override fun unregister() { + networkInfoReceiver.isConnectedCallback = null + context.unregisterReceiver(networkInfoReceiver) + } +} + +@TargetApi(android.os.Build.VERSION_CODES.N) +internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Context) : NetworkCallbackStrategy { + + private var hasChangedCallback: (() -> Unit)? = null + private val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + + override fun onLost(network: Network) { + hasChangedCallback?.invoke() + } + + override fun onAvailable(network: Network) { + hasChangedCallback?.invoke() + } + } + + override fun register(hasChanged: () -> Unit) { + hasChangedCallback = hasChanged + conn.registerDefaultNetworkCallback(networkCallback) + } + + override fun unregister() { + hasChangedCallback = null + conn.unregisterNetworkCallback(networkCallback) + } +} 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 4af6fe8b1c..bacd381e34 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 @@ -16,23 +16,15 @@ package im.vector.matrix.android.internal.network -import android.content.Context import androidx.annotation.WorkerThread -import com.novoda.merlin.Endpoint -import com.novoda.merlin.Merlin -import com.novoda.merlin.MerlinsBeard -import com.novoda.merlin.ResponseCodeValidator -import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.homeserver.HomeServerPinger import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber +import kotlinx.coroutines.runBlocking import java.util.Collections import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine interface NetworkConnectivityChecker { /** @@ -41,60 +33,29 @@ interface NetworkConnectivityChecker { @WorkerThread fun hasInternetAccess(forcePing: Boolean): Boolean - /** - * Wait until we get internet connection. - */ - suspend fun waitUntilConnected() - fun register(listener: Listener) fun unregister(listener: Listener) interface Listener { - fun onConnect() { - } - - fun onDisconnect() { - } + fun onConnectivityChanged() } } @SessionScope -internal class MerlinNetworkConnectivityChecker @Inject constructor(context: Context, - homeServerConnectionConfig: HomeServerConnectionConfig, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val backgroundDetectionObserver: BackgroundDetectionObserver) - : NetworkConnectivityChecker { - - private val waitingForNetwork = AtomicBoolean(false) - private val isMerlinBounded = AtomicBoolean(false) - private val endpointString = "${homeServerConnectionConfig.homeServerUri}/_matrix/client/versions" - private val endpoint = Endpoint.from(endpointString) - private val responseCodeValidator = ResponseCodeValidator { responseCode -> - responseCode == 204 || responseCode == 400 || responseCode == 404 - } - - private val merlin = Merlin.Builder() - .withEndpoint(endpoint) - .withResponseCodeValidator(responseCodeValidator) - .withAllCallbacks() - .build(context) - - private val merlinsBeard = MerlinsBeard.Builder() - .withEndpoint(endpoint) - .withResponseCodeValidator(responseCodeValidator) - .build(context) - - private val hasInternetAccess = AtomicBoolean(merlinsBeard.isConnected) +internal class DefaultNetworkConnectivityChecker @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val homeServerPinger: HomeServerPinger, + private val backgroundDetectionObserver: BackgroundDetectionObserver, + private val networkCallbackStrategy: NetworkCallbackStrategy) : NetworkConnectivityChecker { + private val hasInternetAccess = AtomicBoolean(true) private val listeners = Collections.synchronizedSet(LinkedHashSet()) - private val backgroundDetectionObserverListener = object : BackgroundDetectionObserver.Listener { override fun onMoveToForeground() { - bindMerlinIfNeeded() + bind() } override fun onMoveToBackground() { - unbindMerlinIfNeeded() + unbind() } } @@ -104,81 +65,20 @@ internal class MerlinNetworkConnectivityChecker @Inject constructor(context: Con @WorkerThread override fun hasInternetAccess(forcePing: Boolean): Boolean { return if (forcePing) { - merlinsBeard.hasInternetAccess() + runBlocking { + homeServerPinger.canReachHomeServer() + } } else { hasInternetAccess.get() } } - private fun bindMerlinIfNeeded() { - if (isMerlinBounded.get()) { - return - } - Timber.v("Bind merlin") - isMerlinBounded.set(true) - merlin.bind() - merlinsBeard.hasInternetAccess { - hasInternetAccess.set(it) - } - merlin.registerBindable { - Timber.v("On Network available: ${it.isAvailable}") - } - merlin.registerDisconnectable { - Timber.v("On Disconnect") - hasInternetAccess.set(false) - val localListeners = listeners.toList() - localListeners.forEach { - it.onDisconnect() - } - } - merlin.registerConnectable { - Timber.v("On Connect") - hasInternetAccess.set(true) - val localListeners = listeners.toList() - localListeners.forEach { - it.onConnect() - } - } - } - - private fun unbindMerlinIfNeeded() { - if (backgroundDetectionObserver.isInBackground && !waitingForNetwork.get() && isMerlinBounded.get()) { - isMerlinBounded.set(false) - Timber.v("Unbind merlin") - merlin.unbind() - } - } - - override suspend fun waitUntilConnected() { - val hasInternetAccess = withContext(coroutineDispatchers.io) { - merlinsBeard.hasInternetAccess() - } - if (hasInternetAccess) { - return - } else { - waitingForNetwork.set(true) - bindMerlinIfNeeded() - Timber.v("Waiting for network...") - suspendCoroutine { continuation -> - register(object : NetworkConnectivityChecker.Listener { - override fun onConnect() { - unregister(this) - waitingForNetwork.set(false) - unbindMerlinIfNeeded() - Timber.v("Connected to network...") - continuation.resume(Unit) - } - }) - } - } - } - override fun register(listener: NetworkConnectivityChecker.Listener) { if (listeners.isEmpty()) { if (backgroundDetectionObserver.isInBackground) { - unbindMerlinIfNeeded() + unbind() } else { - bindMerlinIfNeeded() + bind() } backgroundDetectionObserver.register(backgroundDetectionObserverListener) } @@ -191,4 +91,20 @@ internal class MerlinNetworkConnectivityChecker @Inject constructor(context: Con backgroundDetectionObserver.unregister(backgroundDetectionObserverListener) } } + + private fun bind() { + networkCallbackStrategy.register { + val localListeners = listeners.toList() + localListeners.forEach { + it.onConnectivityChanged() + } + } + homeServerPinger.canReachHomeServer { + hasInternetAccess.set(it) + } + } + + private fun unbind() { + networkCallbackStrategy.unregister() + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkInfoReceiver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkInfoReceiver.kt new file mode 100644 index 0000000000..e8daf9b79b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkInfoReceiver.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.internal.network + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkInfo +import javax.inject.Inject + +internal class NetworkInfoReceiver @Inject constructor() : BroadcastReceiver() { + + var isConnectedCallback: ((Boolean) -> Unit)? = null + + override fun onReceive(context: Context, intent: Intent) { + val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkInfo: NetworkInfo? = conn.activeNetworkInfo + isConnectedCallback?.invoke(networkInfo?.isConnected ?: false) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 074a97662b..2569173ae4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.network import im.vector.matrix.android.api.failure.Failure import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay import org.greenrobot.eventbus.EventBus import retrofit2.Call import java.io.IOException @@ -27,6 +28,12 @@ internal suspend inline fun executeRequest(eventBus: EventBus?, internal class Request(private val eventBus: EventBus?) { + var isRetryable = false + var initialDelay: Long = 100L + var maxDelay: Long = 10_000L + var maxRetryCount = Int.MAX_VALUE + private var currentRetryCount = 0 + private var currentDelay = initialDelay lateinit var apiCall: Call suspend fun execute(): DATA { @@ -39,12 +46,18 @@ internal class Request(private val eventBus: EventBus?) { throw response.toFailure(eventBus) } } catch (exception: Throwable) { - throw when (exception) { - is IOException -> Failure.NetworkConnection(exception) - is Failure.ServerError, - is Failure.OtherServerError -> exception - is CancellationException -> Failure.Cancelled(exception) - else -> Failure.Unknown(exception) + if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) { + delay(currentDelay) + currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay) + return execute() + } else { + throw when (exception) { + is IOException -> Failure.NetworkConnection(exception) + is Failure.ServerError, + is Failure.OtherServerError -> exception + is CancellationException -> Failure.Cancelled(exception) + else -> Failure.Unknown(exception) + } } } } 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 1e22df7ab3..d7fb40f006 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 @@ -35,8 +35,11 @@ import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.* import im.vector.matrix.android.internal.network.AccessTokenInterceptor -import im.vector.matrix.android.internal.network.MerlinNetworkConnectivityChecker +import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker +import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy +import im.vector.matrix.android.internal.network.NetworkCallbackStrategy import im.vector.matrix.android.internal.network.NetworkConnectivityChecker +import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater @@ -52,6 +55,7 @@ import okhttp3.OkHttpClient import org.greenrobot.eventbus.EventBus import retrofit2.Retrofit import java.io.File +import javax.inject.Provider @Module internal abstract class SessionModule { @@ -172,13 +176,26 @@ internal abstract class SessionModule { fun providesEventBus(): EventBus { return EventBus.builder().build() } + + @JvmStatic + @Provides + @SessionScope + fun providesNetworkCallbackStrategy(fallbackNetworkCallbackStrategy: Provider, + preferredNetworkCallbackStrategy: Provider + ): NetworkCallbackStrategy { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + preferredNetworkCallbackStrategy.get() + } else { + fallbackNetworkCallbackStrategy.get() + } + } } @Binds abstract fun bindSession(session: DefaultSession): Session @Binds - abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: MerlinNetworkConnectivityChecker): NetworkConnectivityChecker + abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker @Binds @IntoSet @@ -209,3 +226,4 @@ internal abstract class SessionModule { @Binds abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService } + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt index f2015229fd..6531de5585 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt @@ -27,4 +27,10 @@ internal interface CapabilitiesAPI { */ @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") fun getUploadCapabilities(): Call + + /** + * Request the versions + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_+"versions") + fun getVersions(): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeserverPinger.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeserverPinger.kt new file mode 100644 index 0000000000..18c66feb0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeserverPinger.kt @@ -0,0 +1,50 @@ +/* + * 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.session.homeserver + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.TaskExecutor +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal class HomeServerPinger @Inject constructor(private val taskExecutor: TaskExecutor, + private val capabilitiesAPI: CapabilitiesAPI) { + + fun canReachHomeServer(callback: (Boolean) -> Unit) { + taskExecutor.executorScope.launch { + val canReach = canReachHomeServer() + callback(canReach) + } + } + + suspend fun canReachHomeServer(): Boolean { + return try { + executeRequest(null) { + apiCall = capabilitiesAPI.getVersions() + } + true + } catch (throwable: Throwable) { + if (throwable is Failure.OtherServerError) { + (throwable.httpCode == 404 || throwable.httpCode == 400) + } else { + false + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 68a5e30a3f..c6422f5920 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -66,14 +66,14 @@ internal class DefaultSetReadMarkersTask @Inject constructor( val markers = HashMap() Timber.v("Execute set read marker with params: $params") val latestSyncedEventId = latestSyncedEventId(params.roomId) - val fullyReadEventId = if(params.forceReadMarker){ + val fullyReadEventId = if (params.forceReadMarker) { latestSyncedEventId - }else { + } else { params.fullyReadEventId } - val readReceiptEventId = if(params.forceReadReceipt){ + val readReceiptEventId = if (params.forceReadReceipt) { latestSyncedEventId - }else { + } else { params.readReceiptEventId } if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) { @@ -97,8 +97,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor( if (markers.isEmpty()) { return } - networkConnectivityChecker.waitUntilConnected() executeRequest(eventBus) { + isRetryable = true apiCall = roomAPI.sendReadMarker(params.roomId, markers) } } @@ -119,7 +119,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId) readReceiptHandler.handle(realm, roomId, readReceiptContent, false) } - if(shouldUpdateRoomSummary){ + if (shouldUpdateRoomSummary) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() ?: return@awaitTransaction roomSummary.notificationCount = 0 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt index 32f7388d74..ed93c7927f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.room.timeline -import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.filter.FilterRepository import im.vector.matrix.android.internal.session.room.RoomAPI @@ -38,14 +37,13 @@ internal class DefaultPaginationTask @Inject constructor( private val roomAPI: RoomAPI, private val filterRepository: FilterRepository, private val tokenChunkEventPersistor: TokenChunkEventPersistor, - private val eventBus: EventBus, - private val networkConnectivityChecker: NetworkConnectivityChecker + private val eventBus: EventBus ) : PaginationTask { override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { - networkConnectivityChecker.waitUntilConnected() val filter = filterRepository.getRoomFilter() val chunk = executeRequest(eventBus) { + isRetryable = true apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) } return tokenChunkEventPersistor.insertInDb(chunk, params.roomId, params.direction) 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 878d7ca5dc..db197b63df 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 @@ -25,6 +25,8 @@ 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.util.BackgroundDetectionObserver +import im.vector.matrix.android.internal.util.Debouncer +import im.vector.matrix.android.internal.util.createUIHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren @@ -33,7 +35,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import timber.log.Timber import java.net.SocketTimeoutException +import java.util.Timer +import java.util.TimerTask import javax.inject.Inject +import kotlin.concurrent.schedule private const val RETRY_WAIT_TIME_MS = 10_000L private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L @@ -47,9 +52,12 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private var liveState = MutableLiveData() private val lock = Object() private val syncScope = CoroutineScope(SupervisorJob()) + private val debouncer = Debouncer(createUIHandler()) + private var canReachServer = true private var isStarted = false private var isTokenValid = true + private var retryNoNetworkTask: TimerTask? = null init { updateStateTo(SyncState.Idle) @@ -64,7 +72,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (!isStarted) { Timber.v("Resume sync...") isStarted = true - // Check again the token validity + // Check again server availability and the token validity + canReachServer = true isTokenValid = true lock.notify() } @@ -74,6 +83,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (isStarted) { Timber.v("Pause sync...") isStarted = false + retryNoNetworkTask?.cancel() syncScope.coroutineContext.cancelChildren() } } @@ -81,6 +91,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, fun kill() = synchronized(lock) { Timber.v("Kill sync...") updateStateTo(SyncState.Killing) + retryNoNetworkTask?.cancel() syncScope.coroutineContext.cancelChildren() lock.notify() } @@ -89,9 +100,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, return liveState } - override fun onConnect() { - Timber.v("Network is back") + override fun onConnectivityChanged() { + retryNoNetworkTask?.cancel() synchronized(lock) { + canReachServer = true lock.notify() } } @@ -108,11 +120,18 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, updateStateTo(SyncState.Paused) synchronized(lock) { lock.wait() } Timber.v("...unlocked") - } else if (!networkConnectivityChecker.hasInternetAccess(forcePing = false)) { + } else if (!canReachServer) { Timber.v("No network. Waiting...") updateStateTo(SyncState.NoNetwork) + // We force retrying in RETRY_WAIT_TIME_MS maximum. Otherwise it will be unlocked by onConnectivityChanged() or restart() + retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) { + synchronized(lock) { + canReachServer = true + lock.notify() + } + } synchronized(lock) { lock.wait() } - Timber.v("...unlocked") + Timber.v("...retry") } else if (!isTokenValid) { Timber.v("Token is invalid. Waiting...") updateStateTo(SyncState.InvalidToken) @@ -145,6 +164,9 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, try { syncTask.execute(params) } catch (failure: Throwable) { + if (failure is Failure.NetworkConnection) { + canReachServer = false + } if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) { // Timeout are not critical Timber.v("Timeout") @@ -175,7 +197,9 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private fun updateStateTo(newState: SyncState) { Timber.v("Update state from $state to $newState") state = newState - liveState.postValue(newState) + debouncer.debounce("post_state", Runnable { + liveState.value = newState + }, 150) } override fun onMoveToForeground() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt index b87d2df191..1f8cb5cc60 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt @@ -36,7 +36,6 @@ internal data class ConfigurableTask( val id: UUID, val callbackThread: TaskThread, val executionThread: TaskThread, - val retryCount: Int, val callback: MatrixCallback ) : Task by task { @@ -57,7 +56,6 @@ internal data class ConfigurableTask( id = id, callbackThread = callbackThread, executionThread = executionThread, - retryCount = retryCount, callback = callback ) } 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 8dcc15dec6..705073fde5 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 @@ -19,13 +19,11 @@ package im.vector.matrix.android.internal.task import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.di.MatrixScope import im.vector.matrix.android.internal.extensions.foldToCallback -import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.toCancelable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -43,10 +41,8 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers val resultOrFailure = runCatching { withContext(task.executionThread.toDispatcher()) { Timber.v("Enqueue task $task") - retry(task.retryCount) { - Timber.v("Execute task $task on ${Thread.currentThread().name}") - task.execute(task.params) - } + Timber.v("Execute task $task on ${Thread.currentThread().name}") + task.execute(task.params) } } resultOrFailure @@ -60,25 +56,6 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers fun cancelAll() = executorScope.coroutineContext.cancelChildren() - private suspend fun retry( - times: Int = Int.MAX_VALUE, - initialDelay: Long = 100, // 0.1 second - maxDelay: Long = 10_000, // 10 second - factor: Double = 2.0, - block: suspend () -> T): T { - var currentDelay = initialDelay - repeat(times - 1) { - try { - return block() - } catch (e: Exception) { - Timber.v("Retry task after $currentDelay ms") - delay(currentDelay) - currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) - } - } - return block() - } - private fun TaskThread.toDispatcher() = when (this) { TaskThread.MAIN -> coroutineDispatchers.main TaskThread.COMPUTATION -> coroutineDispatchers.computation 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 index 7ca96c9c13..520bed8001 100644 --- a/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt +++ b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt @@ -67,6 +67,7 @@ class VectorSyncService : SyncService() { } private fun reschedule(sessionId: String, delay: Long) { + AlarmSyncBroadcastReceiver.cancelAlarm(applicationContext) AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext, sessionId, delay) } } From 7bcae753149d286c98ab0576bf6aa5efdedcae2d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Jan 2020 20:31:18 +0100 Subject: [PATCH 08/34] Remove some code from main thread --- .../android/internal/session/SessionModule.kt | 3 +++ .../home/room/detail/RoomDetailFragment.kt | 4 ++-- .../home/room/detail/RoomDetailViewModel.kt | 19 +++++++++++++++++++ .../home/room/detail/RoomDetailViewState.kt | 2 ++ 4 files changed, 26 insertions(+), 2 deletions(-) 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 d7fb40f006..af04f901cb 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 @@ -79,6 +79,7 @@ internal abstract class SessionModule { @JvmStatic @UserId @Provides + @SessionScope fun providesUserId(credentials: Credentials): String { return credentials.userId } @@ -86,6 +87,7 @@ internal abstract class SessionModule { @JvmStatic @UserMd5 @Provides + @SessionScope fun providesUserMd5(@UserId userId: String): String { return userId.md5() } @@ -93,6 +95,7 @@ internal abstract class SessionModule { @JvmStatic @SessionId @Provides + @SessionScope fun providesSessionId(credentials: Credentials): String { return credentials.sessionId() } 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 45d1ec1c71..bb54d55cc3 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 @@ -311,7 +311,7 @@ class RoomDetailFragment @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .subscribe { when (it) { - is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) } } @@ -660,7 +660,7 @@ class RoomDetailFragment @Inject constructor( timelineEventController.update(state) inviteView.visibility = View.GONE val uid = session.myUserId - val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) + val meMember = state.myRoomMember() avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView) } else if (summary?.membership == Membership.INVITE && inviter != null) { inviteView.visibility = View.VISIBLE 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 0a375ac328..70f6745871 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 @@ -32,6 +32,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixPatterns +import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isImageMessage @@ -39,6 +40,7 @@ import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +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.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomSummary @@ -52,6 +54,7 @@ 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.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent +import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx @@ -154,6 +157,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeEventDisplayedActions() observeDrafts() observeUnreadState() + observeMyRoomMember() room.getRoomSummaryLive() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, object : MatrixCallback {}) @@ -161,6 +165,21 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro session.onRoomDisplayed(initialState.roomId) } + private fun observeMyRoomMember() { + val queryParams = roomMemberQueryParams { + this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE) + } + room.rx() + .liveRoomMembers(queryParams) + .map { + it.firstOrNull().toOptional() + } + .unwrap() + .execute { + copy(myRoomMember = it) + } + } + override fun handle(action: RoomDetailAction) { when (action) { is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) 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 43a454d32e..aa92b9a61b 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 @@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async 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.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.sync.SyncState @@ -51,6 +52,7 @@ sealed class UnreadState { data class RoomDetailViewState( val roomId: String, val eventId: String?, + val myRoomMember: Async = Uninitialized, val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, val typingRoomMembers: List? = null, From 1d8b81bb04decabe46b1933261e0f3fbb19a5130 Mon Sep 17 00:00:00 2001 From: Ganard Date: Fri, 24 Jan 2020 18:43:35 +0100 Subject: [PATCH 09/34] Try reworking events/timeline process [WIP] --- .../session/room/timeline/ChunkEntityTest.kt | 2 +- .../internal/crypto/DefaultCryptoService.kt | 3 +- .../database/helper/ChunkEntityHelper.kt | 98 +++--------- .../database/helper/RoomEntityHelper.kt | 24 +-- .../helper/TimelineEventSenderVisitor.kt | 148 ------------------ .../internal/database/mapper/EventMapper.kt | 7 +- .../database/mapper/TimelineEventMapper.kt | 2 +- .../internal/database/model/ChunkEntity.kt | 5 +- .../database/model/CurrentStateEventEntity.kt | 31 ++++ .../internal/database/model/EventEntity.kt | 9 +- .../internal/database/model/RoomEntity.kt | 1 - .../database/model/TimelineEventEntity.kt | 4 +- .../database/query/ChunkEntityQueries.kt | 4 +- .../query/CurrentStateEventEntityQueries.kt | 49 ++++++ .../database/query/EventEntityQueries.kt | 47 +----- .../internal/database/query/ReadQueries.kt | 16 +- .../query/TimelineEventEntityQueries.kt | 50 ++---- .../session/room/RoomAvatarResolver.kt | 7 +- .../session/room/RoomSummaryUpdater.kt | 17 +- .../room/membership/LoadRoomMembersTask.kt | 20 ++- .../membership/RoomDisplayNameResolver.kt | 19 ++- .../room/membership/RoomMemberHelper.kt | 20 +-- .../session/room/prune/PruneEventTask.kt | 10 +- .../session/room/send/LocalEchoRepository.kt | 8 +- .../session/room/state/DefaultStateService.kt | 11 +- .../room/timeline/ClearUnlinkedEventsTask.kt | 7 +- .../session/room/timeline/DefaultTimeline.kt | 39 ++--- .../timeline/TimelineHiddenReadReceipts.kt | 6 +- .../room/timeline/TokenChunkEventPersistor.kt | 77 ++++----- .../internal/session/sync/RoomSyncHandler.kt | 55 ++++--- .../media/ImageMediaViewerActivity.kt | 2 +- 31 files changed, 287 insertions(+), 511 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/CurrentStateEventEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt 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 3980094175..f720672e0b 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 @@ -181,7 +181,7 @@ internal class ChunkEntityTest : InstrumentedTest { direction: PaginationDirection, stateIndexOffset: Int = 0) { events.forEach { event -> - add(roomId, event, direction, stateIndexOffset) + add(roomId, event, direction) } } } 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 783b7eb4ad..6ef7c262fb 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 @@ -60,6 +60,7 @@ import im.vector.matrix.android.internal.crypto.tasks.* import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.query.whereType import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.session.SessionScope @@ -482,7 +483,7 @@ internal class DefaultCryptoService @Inject constructor( */ override fun isRoomEncrypted(roomId: String): Boolean { val encryptionEvent = monarchy.fetchCopied { - EventEntity.where(it, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION).findFirst() + EventEntity.whereType(it, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION).findFirst() } return encryptionEvent != null } 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 3fa355fe3c..034607c75a 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 @@ -17,17 +17,13 @@ package im.vector.matrix.android.internal.database.helper 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.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.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomMemberContent 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 import io.realm.kotlin.createObject internal fun ChunkEntity.deleteOnCascade() { @@ -36,42 +32,11 @@ internal fun ChunkEntity.deleteOnCascade() { this.deleteFromRealm() } -internal fun ChunkEntity.merge(roomId: String, - chunkToMerge: ChunkEntity, - direction: PaginationDirection): List { - assertIsManaged() - val isChunkToMergeUnlinked = chunkToMerge.isUnlinked - val isCurrentChunkUnlinked = isUnlinked +internal fun ChunkEntity.addTimelineEvent(roomId: String, + eventEntity: EventEntity, + direction: PaginationDirection, + roomMemberEvent: Event?): TimelineEventEntity { - if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) { - this.timelineEvents.forEach { it.root?.isUnlinked = false } - } - val eventsToMerge: List - if (direction == PaginationDirection.FORWARDS) { - this.nextToken = chunkToMerge.nextToken - this.isLastForward = chunkToMerge.isLastForward - eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) - } else { - this.prevToken = chunkToMerge.prevToken - this.isLastBackward = chunkToMerge.isLastBackward - eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) - } - 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 -): TimelineEventEntity? { - assertIsManaged() - if (event.eventId != null && timelineEvents.find(event.eventId) != null) { - return null - } var currentDisplayIndex = lastDisplayIndex(direction, 0) if (direction == PaginationDirection.FORWARDS) { currentDisplayIndex += 1 @@ -80,22 +45,10 @@ internal fun ChunkEntity.add(roomId: String, currentDisplayIndex -= 1 backwardsDisplayIndex = currentDisplayIndex } - var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset) - if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) { - currentStateIndex += 1 - forwardsStateIndex = currentStateIndex - } else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) { - val lastEventType = timelineEvents.last()?.root?.type ?: "" - if (EventType.isStateEvent(lastEventType)) { - currentStateIndex -= 1 - backwardsStateIndex = currentStateIndex - } - } - val isChunkUnlinked = isUnlinked val localId = TimelineEventEntity.nextId(realm) - val eventId = event.eventId ?: "" - val senderId = event.senderId ?: "" + val eventId = eventEntity.eventId + val senderId = eventEntity.sender ?: "" val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() ?: realm.createObject(eventId).apply { @@ -104,8 +57,9 @@ internal fun ChunkEntity.add(roomId: String, // Update RR for the sender of a new message with a dummy one - if (event.originServerTs != null) { - val timestampOfEvent = event.originServerTs.toDouble() + val originServerTs = eventEntity.originServerTs + if (originServerTs != null) { + val timestampOfEvent = originServerTs.toDouble() val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) // If the synced RR is older, update if (timestampOfEvent > readReceiptOfSender.originServerTs) { @@ -117,23 +71,24 @@ internal fun ChunkEntity.add(roomId: String, } } - val rootEvent = event.toEntity(roomId).apply { - this.stateIndex = currentStateIndex - this.displayIndex = currentDisplayIndex - this.sendState = SendState.SYNCED - this.isUnlinked = isChunkUnlinked - } - val eventEntity = realm.createObject().also { + val timelineEventEntity = TimelineEventEntity().also { it.localId = localId - it.root = realm.copyToRealm(rootEvent) + it.root = realm.copyToRealm(eventEntity) it.eventId = eventId it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() it.readReceipts = readReceiptsSummaryEntity + it.displayIndex = currentDisplayIndex } - val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size - timelineEvents.add(position, eventEntity) - return eventEntity + if (roomMemberEvent != null) { + val roomMemberContent = roomMemberEvent.content.toModel() + timelineEventEntity.senderAvatar = roomMemberContent?.avatarUrl + timelineEventEntity.senderName = roomMemberContent?.displayName + timelineEventEntity.isUniqueDisplayName = false + timelineEventEntity.senderMembershipEventId = roomMemberEvent.eventId + } + timelineEvents.add(timelineEventEntity) + return timelineEventEntity } internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { @@ -142,10 +97,3 @@ internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaul PaginationDirection.BACKWARDS -> backwardsDisplayIndex } ?: defaultValue } - -internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { - return when (direction) { - PaginationDirection.FORWARDS -> forwardsStateIndex - PaginationDirection.BACKWARDS -> backwardsStateIndex - } ?: defaultValue -} 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 9d48d477bf..aa3490d2c4 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 @@ -22,8 +22,6 @@ 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.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.query.fastContains -import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import io.realm.Realm @@ -38,27 +36,9 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) { } } -internal fun RoomEntity.addStateEvent(stateEvent: Event, - stateIndex: Int = Int.MIN_VALUE, - filterDuplicates: Boolean = false, - isUnlinked: Boolean = false) { - assertIsManaged() - if (stateEvent.eventId == null || (filterDuplicates && fastContains(stateEvent.eventId))) { - return - } else { - val entity = stateEvent.toEntity(roomId).apply { - this.stateIndex = stateIndex - this.isUnlinked = isUnlinked - this.sendState = SendState.SYNCED - } - untimelinedStateEvents.add(entity) - } -} internal fun RoomEntity.addSendingEvent(realm: Realm, event: Event) { val senderId = event.senderId ?: return - val eventEntity = event.toEntity(roomId).apply { - this.sendState = SendState.UNSENT - } + val eventEntity = event.toEntity(roomId, SendState.UNSENT) val roomMembers = RoomMemberHelper(realm, roomId) val myUser = roomMembers.getLastRoomMember(senderId) val localId = TimelineEventEntity.nextId(realm) @@ -68,7 +48,7 @@ internal fun RoomEntity.addSendingEvent(realm: Realm, event: Event) { it.roomId = roomId it.senderName = myUser?.displayName it.senderAvatar = myUser?.avatarUrl - it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName) + it.isUniqueDisplayName = roomMembers.isUniqueDisplayName() } sendingTimelineEvents.add(0, timelineEventEntity) } 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 deleted file mode 100644 index 17103ce337..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.database.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.RoomMemberHelper -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 = RoomMemberHelper(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 = RoomMemberHelper(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/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index ed5f04ef75..65fd382960 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.JsonDataException import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.UnsignedData +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.di.MoshiProvider @@ -90,6 +91,8 @@ internal fun EventEntity.asDomain(): Event { return EventMapper.map(this) } -internal fun Event.toEntity(roomId: String): EventEntity { - return EventMapper.map(this, roomId) +internal fun Event.toEntity(roomId: String, sendState: SendState): EventEntity { + return EventMapper.map(this, roomId).apply { + this.sendState = sendState + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 146b5c3ae4..aa282fb673 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -39,7 +39,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS ?: Event("", timelineEventEntity.eventId), annotations = timelineEventEntity.annotations?.asDomain(), localId = timelineEventEntity.localId, - displayIndex = timelineEventEntity.root?.displayIndex ?: 0, + displayIndex = timelineEventEntity.displayIndex, senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, senderAvatar = timelineEventEntity.senderAvatar, 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 94d4a9043f..b1aedbe21a 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 @@ -28,10 +28,7 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false, var backwardsDisplayIndex: Int? = null, - var forwardsDisplayIndex: Int? = null, - var backwardsStateIndex: Int? = null, - var forwardsStateIndex: Int? = null, - var isUnlinked: Boolean = false + var forwardsDisplayIndex: Int? = null ) : RealmObject() { fun identifier() = "${prevToken}_$nextToken" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/CurrentStateEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/CurrentStateEventEntity.kt new file mode 100644 index 0000000000..ba532501b6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/CurrentStateEventEntity.kt @@ -0,0 +1,31 @@ +/* + * 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.database.model + +import io.realm.RealmObject +import io.realm.annotations.Index +import io.realm.annotations.PrimaryKey + +internal open class CurrentStateEventEntity(var eventId: String = "", + var root: EventEntity? = null, + @Index var roomId: String = "", + @Index var type: String = "", + @Index var stateKey: String = "" +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 4def7aec5d..cb64ecf470 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -24,8 +24,9 @@ import io.realm.RealmObject import io.realm.RealmResults import io.realm.annotations.Index import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey -internal open class EventEntity(@Index var eventId: String = "", +internal open class EventEntity(@PrimaryKey var eventId: String = "", @Index var roomId: String = "", @Index var type: String = "", var content: String? = null, @@ -36,9 +37,6 @@ internal open class EventEntity(@Index var eventId: String = "", var age: Long? = 0, var unsignedData: String? = null, var redacts: String? = null, - @Index var stateIndex: Int = 0, - @Index var displayIndex: Int = 0, - @Index var isUnlinked: Boolean = false, var decryptionResultJson: String? = null, var decryptionErrorCode: String? = null ) : RealmObject() { @@ -61,9 +59,6 @@ internal open class EventEntity(@Index var eventId: String = "", companion object - @LinkingObjects("untimelinedStateEvents") - val room: RealmResults? = null - @LinkingObjects("root") val timelineEventEntity: RealmResults? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt index 7de9451c32..6dededff91 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt @@ -23,7 +23,6 @@ import io.realm.annotations.PrimaryKey internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), - var untimelinedStateEvents: RealmList = RealmList(), var sendingTimelineEvents: RealmList = RealmList(), var areAllMembersLoaded: Boolean = false ) : RealmObject() { 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 22f4b9c506..63a11b7ff9 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 @@ -20,10 +20,12 @@ import io.realm.RealmObject import io.realm.RealmResults import io.realm.annotations.Index import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey internal open class TimelineEventEntity(var localId: Long = 0, - @Index var eventId: String = "", + @PrimaryKey var eventId: String = "", @Index var roomId: String = "", + @Index var displayIndex: Int = 0, var root: EventEntity? = null, var annotations: EventAnnotationsSummaryEntity? = null, var senderName: String? = null, 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 b8c058e667..009ee4b7fe 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 @@ -60,12 +60,10 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str internal fun ChunkEntity.Companion.create( realm: Realm, prevToken: String?, - nextToken: String?, - isUnlinked: Boolean + nextToken: String? ): 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/CurrentStateEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt new file mode 100644 index 0000000000..25dec57e46 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt @@ -0,0 +1,49 @@ +/* + * 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.database.query + +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.createObject + +internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: String, stateKey: String, type: String): RealmQuery { + return realm.where(CurrentStateEventEntity::class.java) + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + .equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey) + .equalTo(CurrentStateEventEntityFields.TYPE, type) +} + +internal fun CurrentStateEventEntity.Companion.getOrNull(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity? { + return where(realm, roomId, stateKey, type).findFirst() +} + +internal fun CurrentStateEventEntity.Companion.getOrCreate(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { + return getOrNull(realm, roomId, stateKey, type) ?: create(realm, roomId, stateKey, type) +} + +private fun create(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { + return realm.createObject().apply { + this.type = type + this.roomId = roomId + this.stateKey = stateKey + } +} + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index f3f8db0ea0..590a2162f2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -35,22 +35,15 @@ internal fun EventEntity.Companion.where(realm: Realm, eventIds: List): .`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray()) } -internal fun EventEntity.Companion.where(realm: Realm, - roomId: String? = null, - type: String? = null, - linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery { +internal fun EventEntity.Companion.whereType(realm: Realm, + type: String, + roomId: String? = null +): RealmQuery { val query = realm.where() if (roomId != null) { query.equalTo(EventEntityFields.ROOM_ID, roomId) } - if (type != null) { - query.equalTo(EventEntityFields.TYPE, type) - } - return when (linkFilterMode) { - LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false) - UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true) - BOTH -> query - } + return query.equalTo(EventEntityFields.TYPE, type) } internal fun EventEntity.Companion.types(realm: Realm, @@ -60,36 +53,6 @@ internal fun EventEntity.Companion.types(realm: Realm, return query } -internal fun RealmQuery.descending(since: Int? = null, strict: Boolean = false): RealmQuery { - if (since != null) { - if (strict) { - this.lessThan(EventEntityFields.STATE_INDEX, since) - } else { - this.lessThanOrEqualTo(EventEntityFields.STATE_INDEX, since) - } - } - return this.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) -} - -internal fun RealmQuery.ascending(from: Int? = null, strict: Boolean = true): RealmQuery { - if (from != null) { - if (strict) { - this.greaterThan(EventEntityFields.STATE_INDEX, from) - } else { - this.greaterThanOrEqualTo(EventEntityFields.STATE_INDEX, from) - } - } - return this.sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING) -} - -internal fun RealmQuery.next(from: Int? = null, strict: Boolean = true): EventEntity? { - return this.ascending(from, strict).findFirst() -} - -internal fun RealmQuery.prev(since: Int? = null, strict: Boolean = false): EventEntity? { - return descending(since, strict).findFirst() -} - internal fun RealmList.find(eventId: String): EventEntity? { return this.where() .equalTo(EventEntityFields.EVENT_ID, eventId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt index c214886ec8..bf638654d5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -36,15 +36,15 @@ internal fun isEventRead(monarchy: Monarchy, monarchy.doWithRealm { realm -> val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm - val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root + val eventToCheck = liveChunk.timelineEvents.find(eventId) - isEventRead = if (eventToCheck?.sender == userId) { + isEventRead = if (eventToCheck?.root?.sender == userId) { true } else { val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() - ?: return@doWithRealm - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: return@doWithRealm + val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex + ?: Int.MIN_VALUE val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE eventToCheckIndex <= readReceiptIndex @@ -62,11 +62,11 @@ internal fun isReadMarkerMoreRecent(monarchy: Monarchy, } return Realm.getInstance(monarchy.realmConfiguration).use { realm -> val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false - val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root + val eventToCheck = liveChunk.timelineEvents.find(eventId) val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false - val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.displayIndex + ?: Int.MIN_VALUE val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE eventToCheckIndex <= readMarkerIndex } 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 221e8ccb46..d5e3b13ac2 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 @@ -34,22 +34,20 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, e .`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray()) } -internal fun TimelineEventEntity.Companion.where(realm: Realm, - roomId: String? = null, - type: String? = null, - linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery { +internal fun TimelineEventEntity.Companion.whereRoomId(realm: Realm, + roomId: String): RealmQuery { + return realm.where().equalTo(TimelineEventEntityFields.ROOM_ID, roomId) +} + +internal fun TimelineEventEntity.Companion.whereType(realm: Realm, + type: String, + roomId: String? = null): RealmQuery { val query = realm.where() + query.equalTo(TimelineEventEntityFields.ROOT.TYPE, type) if (roomId != null) { query.equalTo(TimelineEventEntityFields.ROOM_ID, roomId) } - if (type != null) { - query.equalTo(TimelineEventEntityFields.ROOT.TYPE, type) - } - return when (linkFilterMode) { - LINKED_ONLY -> query.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false) - UNLINKED_ONLY -> query.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, true) - BOTH -> query - } + return query } internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List { @@ -71,7 +69,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, liveEvents } return query - ?.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) + ?.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) ?.findFirst() } @@ -83,32 +81,6 @@ internal fun RealmQuery.filterTypes(filterTypes: List.next(from: Int? = null, strict: Boolean = true): TimelineEventEntity? { - if (from != null) { - if (strict) { - this.greaterThan(TimelineEventEntityFields.ROOT.STATE_INDEX, from) - } else { - this.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.STATE_INDEX, from) - } - } - return this - .sort(TimelineEventEntityFields.ROOT.STATE_INDEX, Sort.ASCENDING) - .findFirst() -} - -internal fun RealmQuery.prev(since: Int? = null, strict: Boolean = false): TimelineEventEntity? { - if (since != null) { - if (strict) { - this.lessThan(TimelineEventEntityFields.ROOT.STATE_INDEX, since) - } else { - this.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.STATE_INDEX, since) - } - } - return this - .sort(TimelineEventEntityFields.ROOT.STATE_INDEX, Sort.DESCENDING) - .findFirst() -} - internal fun RealmList.find(eventId: String): TimelineEventEntity? { return this.where() .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) 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 2893bf5126..9f5259bcb6 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 @@ -21,10 +21,9 @@ 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.internal.database.mapper.ContentMapper -import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields -import im.vector.matrix.android.internal.database.query.prev -import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import javax.inject.Inject @@ -40,7 +39,7 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona fun resolve(roomId: String): String? { var res: String? = null monarchy.doWithRealm { realm -> - val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev() + val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "")?.root res = ContentMapper.map(roomName?.content).toModel()?.avatarUrl if (!res.isNullOrEmpty()) { return@doWithRealm 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 bbb5feba15..26626ddfcc 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,11 +24,14 @@ 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.CurrentStateEventEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields 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.getOrCreate +import im.vector.matrix.android.internal.database.query.getOrNull +import im.vector.matrix.android.internal.database.query.isEventRead +import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper @@ -89,10 +92,10 @@ internal class RoomSummaryUpdater @Inject constructor( } 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_ROOM_CANONICAL_ALIAS).prev() - val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() - val encryptionEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ENCRYPTION).prev() + val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root + val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root + val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root + val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 // avoid this call if we are sure there are unread events 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 3610511dbf..e5e5d7ec5a 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,9 +18,11 @@ 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.api.session.room.send.SendState +import im.vector.matrix.android.internal.database.mapper.toEntity +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.getOrCreate 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 @@ -47,7 +49,6 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( private val syncTokenStore: SyncTokenStore, private val roomSummaryUpdater: RoomSummaryUpdater, private val roomMemberEventHandler: RoomMemberEventHandler, - private val timelineEventSenderVisitor: TimelineEventSenderVisitor, private val eventBus: EventBus ) : LoadRoomMembersTask { @@ -69,13 +70,16 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( ?: realm.createObject(roomId) for (roomMemberEvent in response.roomMemberEvents) { - roomEntity.addStateEvent(roomMemberEvent) + if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null) { + continue + } + val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED) + CurrentStateEventEntity.getOrCreate(realm, roomId, roomMemberEvent.stateKey, roomMemberEvent.type).apply { + eventId = roomMemberEvent.eventId + root = eventEntity + } roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) } - timelineEventSenderVisitor.clear() - roomEntity.chunks.flatMap { it.timelineEvents }.forEach { - 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 67b222ecb8..4056de1052 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 @@ -21,14 +21,17 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R 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.api.session.room.model.Membership +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.RoomNameContent 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.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields 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.getOrNull import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId import javax.inject.Inject @@ -57,19 +60,19 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: var name: CharSequence? = null monarchy.doWithRealm { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).prev() + val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root name = ContentMapper.map(roomName?.content).toModel()?.name if (!name.isNullOrEmpty()) { return@doWithRealm } - val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev() + val canonicalAlias = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root name = ContentMapper.map(canonicalAlias?.content).toModel()?.canonicalAlias if (!name.isNullOrEmpty()) { return@doWithRealm } - val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() + val aliases = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root name = ContentMapper.map(aliases?.content).toModel()?.aliases?.firstOrNull() if (!name.isNullOrEmpty()) { return@doWithRealm @@ -126,7 +129,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, roomMemberHelper: RoomMemberHelper): String? { if (roomMemberSummary == null) return null - val isUnique = roomMemberHelper.isUniqueDisplayName(roomMemberSummary.displayName) + val isUnique = roomMemberHelper.isUniqueDisplayName() return if (isUnique) { roomMemberSummary.displayName } else { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt index 029afcbe40..094b4d2530 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt @@ -22,10 +22,10 @@ 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.RoomMemberSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.where import io.realm.Realm import io.realm.RealmQuery -import io.realm.Sort /** * This class is an helper around STATE_ROOM_MEMBER events. @@ -41,11 +41,7 @@ internal class RoomMemberHelper(private val realm: Realm, } fun getLastStateEvent(userId: String): EventEntity? { - return EventEntity - .where(realm, roomId, EventType.STATE_ROOM_MEMBER) - .equalTo(EventEntityFields.STATE_KEY, userId) - .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) - .findFirst() + return CurrentStateEventEntity.getOrNull(realm, roomId, userId, EventType.STATE_ROOM_MEMBER)?.root } fun getLastRoomMember(userId: String): RoomMemberSummaryEntity? { @@ -54,16 +50,8 @@ internal class RoomMemberHelper(private val realm: Realm, .findFirst() } - fun isUniqueDisplayName(displayName: String?): Boolean { - if (displayName.isNullOrEmpty()) { - return true - } - return EventEntity - .where(realm, roomId, EventType.STATE_ROOM_MEMBER) - .contains(EventEntityFields.CONTENT, "\"displayname\":\"$displayName\"") - .distinct(EventEntityFields.STATE_KEY) - .findAll() - .size == 1 + fun isUniqueDisplayName(): Boolean { + return false } fun queryRoomMembersEvent(): RealmQuery { 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 8228136f10..89b5ae5a83 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,6 @@ 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.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,8 +40,7 @@ internal interface PruneEventTask : Task { ) } -internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy, - private val timelineEventSenderVisitor: TimelineEventSenderVisitor) : PruneEventTask { +internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy) : PruneEventTask { override suspend fun execute(params: PruneEventTask.Params) { monarchy.awaitTransaction { realm -> @@ -97,10 +95,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M // } } } + // TODO : make it work again. Maybe waits for SQL rework... if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) { - timelineEventSenderVisitor.clear(roomId = eventToPrune.roomId, senderId = stateKey) - val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId) - timelineEventSenderVisitor.visit(timelineEventsToUpdate) + TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt index 5cdf4d1d4f..ab014596b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -52,9 +52,7 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = listOf(eventId))) monarchy.awaitTransaction { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction - val eventEntity = event.toEntity(roomId).apply { - this.sendState = SendState.UNSENT - } + val eventEntity = event.toEntity(roomId, SendState.UNSENT) val roomMemberHelper = RoomMemberHelper(realm, roomId) val myUser = roomMemberHelper.getLastRoomMember(senderId) val localId = TimelineEventEntity.nextId(realm) @@ -64,7 +62,7 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon it.roomId = roomId it.senderName = myUser?.displayName it.senderAvatar = myUser?.avatarUrl - it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName) + it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName() } roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomSummaryUpdater.update(realm, roomId) @@ -103,7 +101,7 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon return Realm.getInstance(monarchy.realmConfiguration).use { realm -> TimelineEventEntity .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) - .sortedByDescending { it.root?.displayIndex ?: 0 } + .sortedByDescending { it.displayIndex } .mapNotNull { it.root?.asDomain() } .filter { event -> when (event.getClearType()) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index eb7208ea0d..9b027ede6f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -29,9 +29,8 @@ import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM 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.descending -import im.vector.matrix.android.internal.database.query.prev +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity +import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -51,14 +50,14 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private override fun getStateEvent(eventType: String): Event? { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - EventEntity.where(realm, roomId, eventType).prev()?.asDomain() + CurrentStateEventEntity.getOrNull(realm, roomId, type = eventType, stateKey = "")?.root?.asDomain() } } override fun getStateEventLive(eventType: String): LiveData> { val liveData = monarchy.findAllMappedWithChanges( - { realm -> EventEntity.where(realm, roomId, eventType).descending() }, - { it.asDomain() } + { realm -> CurrentStateEventEntity.where(realm, roomId, type = eventType, stateKey = "") }, + { it.root?.asDomain() } ) return Transformations.map(liveData) { results -> results.firstOrNull().toOptional() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt index b532d61914..57d53ff4a8 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 @@ -31,17 +31,18 @@ internal interface ClearUnlinkedEventsTask : Task + return + /*monarchy.awaitTransaction { localRealm -> val unlinkedChunks = ChunkEntity .where(localRealm, roomId = params.roomId) - .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 62268349b3..3a2415f593 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 @@ -38,6 +38,7 @@ 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 import im.vector.matrix.android.internal.database.query.whereInRoom +import im.vector.matrix.android.internal.database.query.whereRoomId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer @@ -177,7 +178,7 @@ internal class DefaultTimeline( } } - nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll() + nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() filteredEvents = nonFilteredEvents.where() .filterEventsWithSettings() .findAll() @@ -260,7 +261,7 @@ internal class DefaultTimeline( // Otherwise, we should check if the event is in the db, but is hidden because of filters return Realm.getInstance(realmConfiguration).use { localRealm -> val nonFilteredEvents = buildEventQuery(localRealm) - .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .findAll() val nonFilteredEvent = nonFilteredEvents.where() @@ -275,11 +276,11 @@ internal class DefaultTimeline( .findFirst() == null if (isHidden) { - val displayIndex = nonFilteredEvent?.root?.displayIndex + val displayIndex = nonFilteredEvent?.displayIndex if (displayIndex != null) { // Then we are looking for the first displayable event after the hidden one val firstDisplayedEvent = filteredEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) .findFirst() firstDisplayedEvent?.eventId } else { @@ -358,14 +359,14 @@ internal class DefaultTimeline( updateState(Timeline.Direction.FORWARDS) { it.copy( - hasMoreInCache = firstBuiltEvent == null || firstBuiltEvent.displayIndex < firstCacheEvent?.root?.displayIndex ?: Int.MIN_VALUE, + hasMoreInCache = firstBuiltEvent == null || firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE, hasReachedEnd = chunkEntity?.isLastForward ?: false ) } updateState(Timeline.Direction.BACKWARDS) { it.copy( - hasMoreInCache = lastBuiltEvent == null || lastBuiltEvent.displayIndex > lastCacheEvent?.root?.displayIndex ?: Int.MAX_VALUE, + hasMoreInCache = lastBuiltEvent == null || lastBuiltEvent.displayIndex > lastCacheEvent?.displayIndex ?: Int.MAX_VALUE, hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE ) } @@ -440,14 +441,14 @@ internal class DefaultTimeline( var shouldFetchInitialEvent = false val currentInitialEventId = initialEventId val initialDisplayIndex = if (currentInitialEventId == null) { - filteredEvents.firstOrNull()?.root?.displayIndex + filteredEvents.firstOrNull()?.displayIndex } else { val initialEvent = nonFilteredEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) .findFirst() shouldFetchInitialEvent = initialEvent == null - initialEvent?.root?.displayIndex + initialEvent?.displayIndex } prevDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex @@ -476,9 +477,9 @@ internal class DefaultTimeline( var postSnapshot = false changeSet.insertionRanges.forEach { range -> val (startDisplayIndex, direction) = if (range.startIndex == 0) { - Pair(filteredEvents[range.length - 1]!!.root!!.displayIndex, Timeline.Direction.FORWARDS) + Pair(filteredEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) } else { - Pair(filteredEvents[range.startIndex]!!.root!!.displayIndex, Timeline.Direction.BACKWARDS) + Pair(filteredEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) } val state = getState(direction) if (state.isPaginating) { @@ -579,7 +580,7 @@ internal class DefaultTimeline( if (offsetResults.isEmpty()) { return 0 } - val offsetIndex = offsetResults.last()!!.root!!.displayIndex + val offsetIndex = offsetResults.last()!!.displayIndex if (direction == Timeline.Direction.BACKWARDS) { prevDisplayIndex = offsetIndex - 1 } else { @@ -620,18 +621,18 @@ internal class DefaultTimeline( strict: Boolean): RealmResults { val offsetQuery = filteredEvents.where() if (direction == Timeline.Direction.BACKWARDS) { - offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) + offsetQuery.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) if (strict) { - offsetQuery.lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + offsetQuery.lessThan(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) } else { - offsetQuery.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + offsetQuery.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) } } else { - offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) + offsetQuery.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) if (strict) { - offsetQuery.greaterThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + offsetQuery.greaterThan(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) } else { - offsetQuery.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + offsetQuery.greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) } } return offsetQuery @@ -642,11 +643,11 @@ internal class DefaultTimeline( private fun buildEventQuery(realm: Realm): RealmQuery { return if (initialEventId == null) { TimelineEventEntity - .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY) + .whereRoomId(realm, roomId = roomId) .equalTo("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true) } else { TimelineEventEntity - .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) + .whereRoomId(realm, roomId = roomId) .`in`("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID}", arrayOf(initialEventId)) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 1a1f90c7a3..05912ba240 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -76,12 +76,12 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue val isLoaded = nonFilteredEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null - val displayIndex = timelineEvent.root?.displayIndex + val displayIndex = timelineEvent.displayIndex - if (isLoaded && displayIndex != null) { + if (isLoaded) { // Then we are looking for the first displayable event after the hidden one val firstDisplayedEvent = filteredEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) .findFirst() // If we find one, we should 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 87c59e832b..177cfcdbbc 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 @@ -17,12 +17,18 @@ package im.vector.matrix.android.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy +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.room.send.SendState import im.vector.matrix.android.internal.database.helper.* +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.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find -import im.vector.matrix.android.internal.database.query.findAllIncludingEvents +import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.kotlin.createObject @@ -32,8 +38,7 @@ import javax.inject.Inject /** * Insert Chunk in DB, and eventually merge with existing chunk event */ -internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy, - private val timelineEventSenderVisitor: TimelineEventSenderVisitor) { +internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy) { /** *
@@ -110,7 +115,6 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
         monarchy
                 .awaitTransaction { realm ->
                     Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
-
                     val roomEntity = RoomEntity.where(realm, roomId).findFirst()
                             ?: realm.createObject(roomId)
 
@@ -124,50 +128,44 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
                         prevToken = receivedChunk.end
                     }
 
-                    val shouldSkip = ChunkEntity.find(realm, roomId, nextToken = nextToken) != 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 which is unlinked (not live)
-
-                    var currentChunk = if (direction == PaginationDirection.FORWARDS) {
+                    val currentChunk = if (direction == PaginationDirection.FORWARDS) {
                         prevChunk?.apply { this.nextToken = nextToken }
                     } else {
                         nextChunk?.apply { this.prevToken = prevToken }
                     }
-                            ?: ChunkEntity.create(realm, prevToken, nextToken, isUnlinked = true)
+                            ?: ChunkEntity.create(realm, prevToken, nextToken)
 
                     if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
                         Timber.v("Reach end of $roomId")
                         currentChunk.isLastBackward = true
-                    } else if (!shouldSkip) {
+                    } else {
                         Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
-                        val timelineEvents = receivedChunk.events.mapNotNull {
-                            currentChunk.add(roomId, it, direction)
-                        }
-                        // Then we merge chunks if needed
-                        if (currentChunk != prevChunk && prevChunk != null) {
-                            currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
-                        } else if (currentChunk != nextChunk && nextChunk != null) {
-                            currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk)
+                        val roomMemberEventsByUser = HashMap()
+                        val eventList = if (direction == PaginationDirection.FORWARDS) {
+                            receivedChunk.events
                         } else {
-                            val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
-                            val overlappedChunks = ChunkEntity.findAllIncludingEvents(realm, newEventIds)
-                            overlappedChunks
-                                    .filter { it != currentChunk }
-                                    .forEach { overlapped ->
-                                        currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped)
-                                    }
+                            receivedChunk.events.asReversed()
+                        }
+                        for (event in eventList) {
+                            if (event.eventId == null || event.senderId == null) {
+                                continue
+                            }
+                            val eventEntity = event.toEntity(roomId, SendState.SYNCED)
+                            if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
+                                roomMemberEventsByUser[event.stateKey] = event
+                            }
+                            val roomMemberEvent = roomMemberEventsByUser.getOrPut(event.senderId) {
+                                CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain()
+                            }
+                            currentChunk.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberEvent)
                         }
                         roomEntity.addOrUpdate(currentChunk)
-                        for (stateEvent in receivedChunk.stateEvents) {
-                            roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked)
-                        }
-                        timelineEventSenderVisitor.visit(timelineEvents)
                     }
                 }
         return if (receivedChunk.events.isEmpty()) {
@@ -180,23 +178,4 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
             Result.SUCCESS
         }
     }
-
-    private fun handleMerge(roomEntity: RoomEntity,
-                            direction: PaginationDirection,
-                            currentChunk: ChunkEntity,
-                            otherChunk: ChunkEntity): ChunkEntity {
-        // 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) {
-            val events = currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
-            timelineEventSenderVisitor.visit(events)
-            roomEntity.deleteOnCascade(otherChunk)
-            currentChunk
-        } else {
-            val events = otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
-            timelineEventSenderVisitor.visit(events)
-            roomEntity.deleteOnCascade(currentChunk)
-            otherChunk
-        }
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
index 9a24eb502a..77762dfb89 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
@@ -22,14 +22,18 @@ 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.tag.RoomTagContent
+import im.vector.matrix.android.api.session.room.send.SendState
 import im.vector.matrix.android.internal.crypto.DefaultCryptoService
 import im.vector.matrix.android.internal.database.helper.*
+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.EventEntityFields
+import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
 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.getOrCreate
+import im.vector.matrix.android.internal.database.query.getOrNull
 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
@@ -52,7 +56,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val roomFullyReadHandler: RoomFullyReadHandler,
                                                    private val cryptoService: DefaultCryptoService,
                                                    private val roomMemberEventHandler: RoomMemberEventHandler,
-                                                   private val timelineEventSenderVisitor: TimelineEventSenderVisitor,
                                                    private val eventBus: EventBus) {
 
     sealed class HandlingStrategy {
@@ -118,13 +121,16 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         roomEntity.membership = Membership.JOIN
 
         // State event
-
         if (roomSync.state?.events?.isNotEmpty() == true) {
-            val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
-                    ?: Int.MIN_VALUE
-            val untimelinedStateIndex = minStateIndex + 1
-            roomSync.state.events.forEach { event ->
-                roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
+            for (event in roomSync.state.events) {
+                if (event.eventId == null || event.stateKey == null) {
+                    continue
+                }
+                val eventEntity = event.toEntity(roomId, SendState.SYNCED)
+                CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
+                    eventId = event.eventId
+                    root = eventEntity
+                }
                 // Give info to crypto module
                 cryptoService.onStateEvent(roomId, event)
                 roomMemberEventHandler.handle(realm, roomId, event)
@@ -193,28 +199,37 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                      prevToken: String? = null,
                                      isLimited: Boolean = true): ChunkEntity {
         val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
-        var stateIndexOffset = 0
         val chunkEntity = if (!isLimited && lastChunk != null) {
             lastChunk
         } else {
             realm.createObject().apply { this.prevToken = prevToken }
         }
-        if (isLimited && lastChunk != null) {
-            stateIndexOffset = lastChunk.lastStateIndex(PaginationDirection.FORWARDS)
-        }
         lastChunk?.isLastForward = false
         chunkEntity.isLastForward = true
-        chunkEntity.isUnlinked = false
 
-        val timelineEvents = ArrayList(eventList.size)
         val eventIds = ArrayList(eventList.size)
+        val roomMemberEventsByUser = HashMap()
+
         for (event in eventList) {
-            if(event.eventId != null) {
-                eventIds.add(event.eventId)
+            if (event.eventId == null || event.senderId == null) {
+                continue
             }
-            chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also {
-                timelineEvents.add(it)
+            eventIds.add(event.eventId)
+            val eventEntity = event.toEntity(roomId, SendState.SYNCED)
+            if (event.isStateEvent() && event.stateKey != null) {
+                CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
+                    eventId = event.eventId
+                    root = eventEntity
+                }
+                if (event.type == EventType.STATE_ROOM_MEMBER) {
+                    roomMemberEventsByUser[event.stateKey] = event
+                    roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
+                }
             }
+            val roomMemberEvent = roomMemberEventsByUser.getOrPut(event.senderId) {
+                CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain()
+            }
+            chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberEvent)
             // Give info to crypto module
             cryptoService.onLiveEvent(roomEntity.roomId, event)
             // Try to remove local echo
@@ -227,9 +242,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     Timber.v("Can't find corresponding local echo for tx:$it")
                 }
             }
-            roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
         }
-        timelineEventSenderVisitor.visit(timelineEvents)
         // posting new events to timeline if any is registered
         eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = eventIds))
         return chunkEntity
diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
index 45ceeed49a..d9da08254f 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
@@ -61,7 +61,7 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
             ViewCompat.setTransitionName(imageTransitionView, it)
         }
         if (mediaData.url.isNullOrEmpty()) {
-            finish()
+            supportFinishAfterTransition()
             return
         }
 

From 3a89a30056d33e1d374de0d82c845c15f386babd Mon Sep 17 00:00:00 2001
From: ganfra 
Date: Sat, 25 Jan 2020 18:59:45 +0100
Subject: [PATCH 10/34] Continue reworking sync/timeline events handling

---
 .../database/helper/ChunkEntityHelper.kt      | 34 ++++++-----
 .../database/model/SessionRealmModule.kt      | 61 ++++++++++---------
 .../room/membership/LoadRoomMembersTask.kt    |  4 +-
 .../room/timeline/TokenChunkEventPersistor.kt | 31 ++++++++--
 .../internal/session/sync/RoomSyncHandler.kt  | 10 ++-
 5 files changed, 84 insertions(+), 56 deletions(-)

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 034607c75a..506e37891d 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
@@ -19,7 +19,13 @@ package im.vector.matrix.android.internal.database.helper
 import im.vector.matrix.android.api.session.events.model.Event
 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.model.*
+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.EventEntity
+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.query.getOrCreate
 import im.vector.matrix.android.internal.database.query.where
 import im.vector.matrix.android.internal.extensions.assertIsManaged
@@ -37,15 +43,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
                                           direction: PaginationDirection,
                                           roomMemberEvent: Event?): TimelineEventEntity {
 
-    var currentDisplayIndex = lastDisplayIndex(direction, 0)
-    if (direction == PaginationDirection.FORWARDS) {
-        currentDisplayIndex += 1
-        forwardsDisplayIndex = currentDisplayIndex
-    } else {
-        currentDisplayIndex -= 1
-        backwardsDisplayIndex = currentDisplayIndex
-    }
-
+    val displayIndex = nextDisplayIndex(direction)
     val localId = TimelineEventEntity.nextId(realm)
     val eventId = eventEntity.eventId
     val senderId = eventEntity.sender ?: ""
@@ -73,12 +71,12 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
 
     val timelineEventEntity = TimelineEventEntity().also {
         it.localId = localId
-        it.root = realm.copyToRealm(eventEntity)
+        it.root = eventEntity
         it.eventId = eventId
         it.roomId = roomId
         it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
         it.readReceipts = readReceiptsSummaryEntity
-        it.displayIndex = currentDisplayIndex
+        it.displayIndex = displayIndex
     }
     if (roomMemberEvent != null) {
         val roomMemberContent = roomMemberEvent.content.toModel()
@@ -91,9 +89,13 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
     return timelineEventEntity
 }
 
-internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
+internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int {
     return when (direction) {
-        PaginationDirection.FORWARDS  -> forwardsDisplayIndex
-        PaginationDirection.BACKWARDS -> backwardsDisplayIndex
-    } ?: defaultValue
+        PaginationDirection.FORWARDS  -> {
+            (timelineEvents.where().max(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) + 1
+        }
+        PaginationDirection.BACKWARDS -> {
+            (timelineEvents.where().min(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) - 1
+        }
+    }
 }
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 a29e7e89fb..48c16bc26a 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
@@ -22,34 +22,35 @@ import io.realm.annotations.RealmModule
  * Realm module for Session
  */
 @RealmModule(library = true,
-             classes = [
-                 ChunkEntity::class,
-                 EventEntity::class,
-                 TimelineEventEntity::class,
-                 FilterEntity::class,
-                 GroupEntity::class,
-                 GroupSummaryEntity::class,
-                 ReadReceiptEntity::class,
-                 RoomEntity::class,
-                 RoomSummaryEntity::class,
-                 RoomTagEntity::class,
-                 SyncEntity::class,
-                 UserEntity::class,
-                 IgnoredUserEntity::class,
-                 BreadcrumbsEntity::class,
-                 EventAnnotationsSummaryEntity::class,
-                 ReactionAggregatedSummaryEntity::class,
-                 EditAggregatedSummaryEntity::class,
-                 PushRulesEntity::class,
-                 PushRuleEntity::class,
-                 PushConditionEntity::class,
-                 PusherEntity::class,
-                 PusherDataEntity::class,
-                 ReadReceiptsSummaryEntity::class,
-                 ReadMarkerEntity::class,
-                 UserDraftsEntity::class,
-                 DraftEntity::class,
-                 HomeServerCapabilitiesEntity::class,
-                 RoomMemberSummaryEntity::class
-             ])
+        classes = [
+            ChunkEntity::class,
+            EventEntity::class,
+            TimelineEventEntity::class,
+            FilterEntity::class,
+            GroupEntity::class,
+            GroupSummaryEntity::class,
+            ReadReceiptEntity::class,
+            RoomEntity::class,
+            RoomSummaryEntity::class,
+            RoomTagEntity::class,
+            SyncEntity::class,
+            UserEntity::class,
+            IgnoredUserEntity::class,
+            BreadcrumbsEntity::class,
+            EventAnnotationsSummaryEntity::class,
+            ReactionAggregatedSummaryEntity::class,
+            EditAggregatedSummaryEntity::class,
+            PushRulesEntity::class,
+            PushRuleEntity::class,
+            PushConditionEntity::class,
+            PusherEntity::class,
+            PusherDataEntity::class,
+            ReadReceiptsSummaryEntity::class,
+            ReadMarkerEntity::class,
+            UserDraftsEntity::class,
+            DraftEntity::class,
+            HomeServerCapabilitiesEntity::class,
+            RoomMemberSummaryEntity::class,
+            CurrentStateEventEntity::class
+        ])
 internal class SessionRealmModule
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 e5e5d7ec5a..5dfe54a7bb 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
@@ -73,7 +73,9 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
                 if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null) {
                     continue
                 }
-                val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED)
+                val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED).let {
+                    realm.copyToRealmOrUpdate(it)
+                }
                 CurrentStateEventEntity.getOrCreate(realm, roomId, roomMemberEvent.stateKey, roomMemberEvent.type).apply {
                     eventId = roomMemberEvent.eventId
                     root = eventEntity
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 177cfcdbbc..1e75a52133 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
@@ -20,11 +20,13 @@ import com.zhuinden.monarchy.Monarchy
 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.room.send.SendState
-import im.vector.matrix.android.internal.database.helper.*
+import im.vector.matrix.android.internal.database.helper.addOrUpdate
+import im.vector.matrix.android.internal.database.helper.addTimelineEvent
 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.CurrentStateEventEntity
+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.query.create
 import im.vector.matrix.android.internal.database.query.find
@@ -152,18 +154,35 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
                         } else {
                             receivedChunk.events.asReversed()
                         }
+                        val stateEvents = receivedChunk.stateEvents
+                        for (stateEvent in stateEvents) {
+                            if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null && !stateEvent.isRedacted()) {
+                                roomMemberEventsByUser[stateEvent.stateKey] = stateEvent
+                            }
+                        }
+                        val eventEntities = ArrayList(eventList.size)
                         for (event in eventList) {
                             if (event.eventId == null || event.senderId == null) {
                                 continue
                             }
-                            val eventEntity = event.toEntity(roomId, SendState.SYNCED)
-                            if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
+                            val eventEntity = event.toEntity(roomId, SendState.SYNCED).also {
+                                realm.copyToRealmOrUpdate(it)
+                            }
+                            if(direction == PaginationDirection.FORWARDS){
+                                eventEntities.add(eventEntity)
+                            }else {
+                                eventEntities.add(0, eventEntity)
+                            }
+                            if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null && !event.isRedacted()) {
                                 roomMemberEventsByUser[event.stateKey] = event
                             }
-                            val roomMemberEvent = roomMemberEventsByUser.getOrPut(event.senderId) {
-                                CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain()
+                        }
+                        for (eventEntity in eventEntities) {
+                            val senderId = eventEntity.sender ?: continue
+                            val roomMemberEvent = roomMemberEventsByUser.getOrPut(senderId) {
+                                CurrentStateEventEntity.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain()
                             }
-                            currentChunk.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberEvent)
+                            currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberEvent)
                         }
                         roomEntity.addOrUpdate(currentChunk)
                     }
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 77762dfb89..c6f8525a42 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
@@ -126,7 +126,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 if (event.eventId == null || event.stateKey == null) {
                     continue
                 }
-                val eventEntity = event.toEntity(roomId, SendState.SYNCED)
+                val eventEntity = event.toEntity(roomId, SendState.SYNCED).let {
+                    realm.copyToRealmOrUpdate(it)
+                }
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
                     eventId = event.eventId
                     root = eventEntity
@@ -215,8 +217,10 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 continue
             }
             eventIds.add(event.eventId)
-            val eventEntity = event.toEntity(roomId, SendState.SYNCED)
-            if (event.isStateEvent() && event.stateKey != null) {
+            val eventEntity = event.toEntity(roomId, SendState.SYNCED).let {
+                realm.copyToRealmOrUpdate(it)
+            }
+            if (event.isStateEvent() && event.stateKey != null && !event.isRedacted()) {
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
                     eventId = event.eventId
                     root = eventEntity

From f3db43f3179a84982ade645237a34fe9e2a6f346 Mon Sep 17 00:00:00 2001
From: Ganard 
Date: Mon, 27 Jan 2020 12:41:43 +0100
Subject: [PATCH 11/34] Ellipsize: introduce EllipsizingTextView

---
 .../core/platform/EllipsizingTextView.java    | 445 ++++++++++++++++++
 ...onstraint_set_composer_layout_expanded.xml |   2 +-
 .../layout/fragment_create_direct_room.xml    |   2 +-
 ...ent_create_direct_room_directory_users.xml |   2 +-
 .../main/res/layout/fragment_create_room.xml  |   2 +-
 .../main/res/layout/fragment_home_detail.xml  |   2 +-
 .../res/layout/fragment_matrix_profile.xml    |   2 +-
 .../main/res/layout/fragment_room_detail.xml  |   4 +-
 .../res/layout/fragment_room_member_list.xml  |   2 +-
 .../fragment_room_preview_no_preview.xml      |   2 +-
 .../res/layout/item_autocomplete_emoji.xml    |   2 +-
 .../res/layout/item_bottom_sheet_action.xml   |   2 +-
 .../item_bottom_sheet_message_preview.xml     |   4 +-
 .../layout/item_bottom_sheet_room_preview.xml |   2 +-
 .../layout/item_create_direct_room_user.xml   |   4 +-
 vector/src/main/res/layout/item_device.xml    |   2 +-
 .../src/main/res/layout/item_form_switch.xml  |   2 +-
 vector/src/main/res/layout/item_group.xml     |   2 +-
 .../main/res/layout/item_profile_action.xml   |   4 +-
 .../res/layout/item_profile_matrix_item.xml   |   4 +-
 .../src/main/res/layout/item_public_room.xml  |   2 +-
 vector/src/main/res/layout/item_room.xml      |   6 +-
 .../main/res/layout/item_room_category.xml    |   2 +-
 .../main/res/layout/item_room_directory.xml   |   4 +-
 .../main/res/layout/item_room_invitation.xml  |   4 +-
 .../res/layout/item_timeline_event_base.xml   |   2 +-
 vector/src/main/res/layout/item_user.xml      |   4 +-
 .../main/res/layout/merge_composer_layout.xml |   2 +-
 28 files changed, 482 insertions(+), 37 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.java

diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.java b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.java
new file mode 100644
index 0000000000..bc6332cd58
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.java
@@ -0,0 +1,445 @@
+/*
+ * 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.platform;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.text.Layout;
+import android.text.Layout.Alignment;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.text.style.ForegroundColorSpan;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.AppCompatTextView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link android.widget.TextView} that ellipsizes more intelligently.
+ * This class supports ellipsizing multiline text through setting {@code android:ellipsize}
+ * and {@code android:maxLines}.
+ * 

+ * Note: {@link android.text.TextUtils.TruncateAt#MARQUEE} ellipsizing type is not supported. + * This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues. + */ +public class EllipsizingTextView extends AppCompatTextView { + public static final int ELLIPSIZE_ALPHA = 0x88; + private SpannableString ELLIPSIS = new SpannableString("\u2026"); + + private static final Pattern DEFAULT_END_PUNCTUATION + = Pattern.compile("[\\.!?,;:\u2026]*$", Pattern.DOTALL); + private final List mEllipsizeListeners = new ArrayList<>(); + private EllipsizeStrategy mEllipsizeStrategy; + private boolean isEllipsized; + private boolean isStale; + private boolean programmaticChange; + private CharSequence mFullText; + private int mMaxLines; + private float mLineSpacingMult = 1.0f; + private float mLineAddVertPad = 0.0f; + + /** + * The end punctuation which will be removed when appending {@link #ELLIPSIS}. + */ + private Pattern mEndPunctPattern; + + public EllipsizingTextView(Context context) { + this(context, null); + } + + + public EllipsizingTextView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.textViewStyle); + } + + + public EllipsizingTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + TypedArray a = context.obtainStyledAttributes(attrs, + new int[]{android.R.attr.maxLines, android.R.attr.ellipsize}, defStyle, 0); + setMaxLines(a.getInt(0, Integer.MAX_VALUE)); + a.recycle(); + setEndPunctuationPattern(DEFAULT_END_PUNCTUATION); + final int currentTextColor = getCurrentTextColor(); + final int ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor)); + ELLIPSIS.setSpan(new ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + public void setEndPunctuationPattern(Pattern pattern) { + mEndPunctPattern = pattern; + } + + @SuppressWarnings("unused") + public void addEllipsizeListener(@NonNull EllipsizeListener listener) { + mEllipsizeListeners.add(listener); + } + + @SuppressWarnings("unused") + public void removeEllipsizeListener(@NonNull EllipsizeListener listener) { + mEllipsizeListeners.remove(listener); + } + + @SuppressWarnings("unused") + public boolean isEllipsized() { + return isEllipsized; + } + + /** + * @return The maximum number of lines displayed in this {@link android.widget.TextView}. + */ + public int getMaxLines() { + return mMaxLines; + } + + @Override + public void setMaxLines(int maxLines) { + super.setMaxLines(maxLines); + mMaxLines = maxLines; + isStale = true; + } + + /** + * Determines if the last fully visible line is being ellipsized. + * + * @return {@code true} if the last fully visible line is being ellipsized; + * otherwise, returns {@code false}. + */ + public boolean ellipsizingLastFullyVisibleLine() { + return mMaxLines == Integer.MAX_VALUE; + } + + @Override + public void setLineSpacing(float add, float mult) { + mLineAddVertPad = add; + mLineSpacingMult = mult; + super.setLineSpacing(add, mult); + } + + @Override + public void setText(CharSequence text, BufferType type) { + if (!programmaticChange) { + mFullText = text instanceof Spanned ? (Spanned) text : text; + isStale = true; + } + super.setText(text, type); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (ellipsizingLastFullyVisibleLine()) { + isStale = true; + } + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + super.setPadding(left, top, right, bottom); + if (ellipsizingLastFullyVisibleLine()) { + isStale = true; + } + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + if (isStale) { + resetText(); + } + super.onDraw(canvas); + } + + /** + * Sets the ellipsized text if appropriate. + */ + private void resetText() { + int maxLines = getMaxLines(); + CharSequence workingText = mFullText; + boolean ellipsized = false; + + if (maxLines != -1) { + if (mEllipsizeStrategy == null) setEllipsize(null); + workingText = mEllipsizeStrategy.processText(mFullText); + ellipsized = !mEllipsizeStrategy.isInLayout(mFullText); + } + + if (!workingText.equals(getText())) { + programmaticChange = true; + try { + setText(workingText); + } finally { + programmaticChange = false; + } + } + + isStale = false; + if (ellipsized != isEllipsized) { + isEllipsized = ellipsized; + for (EllipsizeListener listener : mEllipsizeListeners) { + listener.ellipsizeStateChanged(ellipsized); + } + } + } + + /** + * Causes words in the text that are longer than the view is wide to be ellipsized + * instead of broken in the middle. Use {@code null} to turn off ellipsizing. + *

+ * Note: Method does nothing for {@link android.text.TextUtils.TruncateAt#MARQUEE} + * ellipsizing type. + * + * @param where part of text to ellipsize + */ + @Override + public void setEllipsize(TruncateAt where) { + if (where == null) { + mEllipsizeStrategy = new EllipsizeNoneStrategy(); + return; + } + + switch (where) { + case END: + mEllipsizeStrategy = new EllipsizeEndStrategy(); + break; + case START: + mEllipsizeStrategy = new EllipsizeStartStrategy(); + break; + case MIDDLE: + mEllipsizeStrategy = new EllipsizeMiddleStrategy(); + break; + case MARQUEE: + default: + mEllipsizeStrategy = new EllipsizeNoneStrategy(); + break; + } + } + + /** + * A listener that notifies when the ellipsize state has changed. + */ + public interface EllipsizeListener { + void ellipsizeStateChanged(boolean ellipsized); + } + + /** + * A base class for an ellipsize strategy. + */ + private abstract class EllipsizeStrategy { + /** + * Returns ellipsized text if the text does not fit inside of the layout; + * otherwise, returns the full text. + * + * @param text text to process + * @return Ellipsized text if the text does not fit inside of the layout; + * otherwise, returns the full text. + */ + public CharSequence processText(CharSequence text) { + return !isInLayout(text) ? createEllipsizedText(text) : text; + } + + /** + * Determines if the text fits inside of the layout. + * + * @param text text to fit + * @return {@code true} if the text fits inside of the layout; + * otherwise, returns {@code false}. + */ + public boolean isInLayout(CharSequence text) { + Layout layout = createWorkingLayout(text); + return layout.getLineCount() <= getLinesCount(); + } + + /** + * Creates a working layout with the given text. + * + * @param workingText text to create layout with + * @return {@link android.text.Layout} with the given text. + */ + protected Layout createWorkingLayout(CharSequence workingText) { + return new StaticLayout(workingText, getPaint(), + getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(), + Alignment.ALIGN_NORMAL, mLineSpacingMult, + mLineAddVertPad, false /* includepad */); + } + + /** + * Get how many lines of text we are allowed to display. + */ + protected int getLinesCount() { + if (ellipsizingLastFullyVisibleLine()) { + int fullyVisibleLinesCount = getFullyVisibleLinesCount(); + return fullyVisibleLinesCount == -1 ? 1 : fullyVisibleLinesCount; + } else { + return mMaxLines; + } + } + + /** + * Get how many lines of text we can display so their full height is visible. + */ + protected int getFullyVisibleLinesCount() { + Layout layout = createWorkingLayout(""); + int height = getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); + int lineHeight = layout.getLineBottom(0); + return height / lineHeight; + } + + /** + * Creates ellipsized text from the given text. + * + * @param fullText text to ellipsize + * @return Ellipsized text + */ + protected abstract CharSequence createEllipsizedText(CharSequence fullText); + } + + /** + * An {@link EllipsizingTextView.EllipsizeStrategy} that + * does not ellipsize text. + */ + private class EllipsizeNoneStrategy extends EllipsizeStrategy { + @Override + protected CharSequence createEllipsizedText(CharSequence fullText) { + return fullText; + } + } + + /** + * An {@link EllipsizingTextView.EllipsizeStrategy} that + * ellipsizes text at the end. + */ + private class EllipsizeEndStrategy extends EllipsizeStrategy { + @Override + protected CharSequence createEllipsizedText(CharSequence fullText) { + Layout layout = createWorkingLayout(fullText); + int cutOffIndex = layout.getLineEnd(mMaxLines - 1); + int textLength = fullText.length(); + int cutOffLength = textLength - cutOffIndex; + if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length(); + CharSequence workingText = TextUtils.substring(fullText, 0, textLength - cutOffLength).trim(); + + while (!isInLayout(TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS))) { + int lastSpace = TextUtils.lastIndexOf(workingText, ' '); + if (lastSpace == -1) { + break; + } + workingText = TextUtils.substring(workingText, 0, lastSpace).trim(); + } + + workingText = TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS); + SpannableStringBuilder dest = new SpannableStringBuilder(workingText); + + if (fullText instanceof Spanned) { + TextUtils.copySpansFrom((Spanned) fullText, 0, workingText.length(), null, dest, 0); + } + return dest; + } + + /** + * Strips the end punctuation from a given text according to {@link #mEndPunctPattern}. + * + * @param workingText text to strip end punctuation from + * @return Text without end punctuation. + */ + public String stripEndPunctuation(CharSequence workingText) { + return mEndPunctPattern.matcher(workingText).replaceFirst(""); + } + } + + /** + * An {@link EllipsizingTextView.EllipsizeStrategy} that + * ellipsizes text at the start. + */ + private class EllipsizeStartStrategy extends EllipsizeStrategy { + @Override + protected CharSequence createEllipsizedText(CharSequence fullText) { + Layout layout = createWorkingLayout(fullText); + int cutOffIndex = layout.getLineEnd(mMaxLines - 1); + int textLength = fullText.length(); + int cutOffLength = textLength - cutOffIndex; + if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length(); + CharSequence workingText = TextUtils.substring(fullText, cutOffLength, textLength).trim(); + + while (!isInLayout(TextUtils.concat(ELLIPSIS, workingText))) { + int firstSpace = TextUtils.indexOf(workingText, ' '); + if (firstSpace == -1) { + break; + } + workingText = TextUtils.substring(workingText, firstSpace, workingText.length()).trim(); + } + + workingText = TextUtils.concat(ELLIPSIS, workingText); + SpannableStringBuilder dest = new SpannableStringBuilder(workingText); + + if (fullText instanceof Spanned) { + TextUtils.copySpansFrom((Spanned) fullText, textLength - workingText.length(), + textLength, null, dest, 0); + } + return dest; + } + } + + /** + * An {@link EllipsizingTextView.EllipsizeStrategy} that + * ellipsizes text in the middle. + */ + private class EllipsizeMiddleStrategy extends EllipsizeStrategy { + @Override + protected CharSequence createEllipsizedText(CharSequence fullText) { + Layout layout = createWorkingLayout(fullText); + int cutOffIndex = layout.getLineEnd(mMaxLines - 1); + int textLength = fullText.length(); + int cutOffLength = textLength - cutOffIndex; + if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length(); + cutOffLength += cutOffIndex % 2; // Make it even. + String firstPart = TextUtils.substring( + fullText, 0, textLength / 2 - cutOffLength / 2).trim(); + String secondPart = TextUtils.substring( + fullText, textLength / 2 + cutOffLength / 2, textLength).trim(); + + while (!isInLayout(TextUtils.concat(firstPart, ELLIPSIS, secondPart))) { + int lastSpaceFirstPart = firstPart.lastIndexOf(' '); + int firstSpaceSecondPart = secondPart.indexOf(' '); + if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break; + firstPart = firstPart.substring(0, lastSpaceFirstPart).trim(); + secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length()).trim(); + } + + SpannableStringBuilder firstDest = new SpannableStringBuilder(firstPart); + SpannableStringBuilder secondDest = new SpannableStringBuilder(secondPart); + + if (fullText instanceof Spanned) { + TextUtils.copySpansFrom((Spanned) fullText, 0, firstPart.length(), + null, firstDest, 0); + TextUtils.copySpansFrom((Spanned) fullText, textLength - secondPart.length(), + textLength, null, secondDest, 0); + } + return TextUtils.concat(firstDest, ELLIPSIS, secondDest); + } + } +} \ No newline at end of file diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml index 4c9225dba7..d246c988e6 100644 --- a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml +++ b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml @@ -60,7 +60,7 @@ app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/first_names" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Date: Tue, 28 Jan 2020 10:13:19 +0100 Subject: [PATCH 12/34] Fix SyncService and Alarm --- .../internal/session/sync/job/SyncService.kt | 3 --- .../receiver/AlarmSyncBroadcastReceiver.kt | 11 ++++++++++- .../riotx/core/services/VectorSyncService.kt | 18 +++++++++++++++--- 3 files changed, 25 insertions(+), 7 deletions(-) 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 c6845ad0c3..1c870ebf9a 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 @@ -143,9 +143,6 @@ abstract class SyncService : Service() { backgroundDetectionObserver = matrix.backgroundDetectionObserver return true } catch (exception: Exception) { - if (BuildConfig.DEBUG) { - throw exception - } Timber.e(exception, "An exception occurred during initialisation") return false } 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 84895f9f43..cf30e2daa2 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,12 +25,22 @@ 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.core.di.HasVectorInjector import im.vector.riotx.core.services.VectorSyncService import timber.log.Timber class AlarmSyncBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { + val appContext = context.applicationContext + if (appContext is HasVectorInjector) { + val activeSession = appContext.injector().activeSessionHolder().getSafeActiveSession() + if (activeSession == null) { + Timber.v("No active session don't launch sync service.") + return + } + } + // Acquire a lock to give enough time for the sync :/ (context.getSystemService(Context.POWER_SERVICE) as PowerManager).run { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply { @@ -51,7 +61,6 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { } scheduleAlarm(context, sessionId, 30_000L) - Timber.i("Alarm scheduled to restart service") } 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 index 520bed8001..314e12db05 100644 --- a/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt +++ b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt @@ -15,13 +15,15 @@ */ 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.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.riotx.features.notifications.NotificationUtils class VectorSyncService : SyncService() { @@ -67,7 +69,17 @@ class VectorSyncService : SyncService() { } private fun reschedule(sessionId: String, delay: Long) { - AlarmSyncBroadcastReceiver.cancelAlarm(applicationContext) - AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext, sessionId, delay) + val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PendingIntent.getForegroundService(this, 0, newIntent(this, sessionId), 0) + } else { + PendingIntent.getService(this, 0, newIntent(this, sessionId), 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) + } } } From 15b0bea8704decc984eb8fa7c9f93d960a083e78 Mon Sep 17 00:00:00 2001 From: Ganard Date: Tue, 28 Jan 2020 10:13:36 +0100 Subject: [PATCH 13/34] Use clone for retrofit request to be able to retry --- .../java/im/vector/matrix/android/internal/network/Request.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 2569173ae4..9714f42bb0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -38,7 +38,7 @@ internal class Request(private val eventBus: EventBus?) { suspend fun execute(): DATA { return try { - val response = apiCall.awaitResponse() + val response = apiCall.clone().awaitResponse() if (response.isSuccessful) { response.body() ?: throw IllegalStateException("The request returned a null body") From bf7c53ecaba445dec8d8d44fa01088d3b3b88f5e Mon Sep 17 00:00:00 2001 From: Ganard Date: Tue, 28 Jan 2020 14:46:26 +0100 Subject: [PATCH 14/34] Sync/pagination: get a working version --- .../database/helper/ChunkEntityHelper.kt | 61 +++++--- .../internal/database/model/ChunkEntity.kt | 4 +- .../internal/database/model/EventEntity.kt | 10 -- .../database/model/TimelineEventEntity.kt | 2 +- .../database/query/EventEntityQueries.kt | 2 - .../query/TimelineEventEntityQueries.kt | 1 - .../internal/session/room/RoomModule.kt | 3 - .../session/room/RoomSummaryUpdater.kt | 32 +++-- .../room/timeline/ClearUnlinkedEventsTask.kt | 48 ------- .../timeline/DefaultGetContextOfEventTask.kt | 7 +- .../session/room/timeline/DefaultTimeline.kt | 6 +- .../room/timeline/DefaultTimelineService.kt | 4 +- .../room/timeline/TokenChunkEventPersistor.kt | 131 ++++++++++++------ 13 files changed, 157 insertions(+), 154 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt 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 506e37891d..5fd7e07b89 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 @@ -26,10 +26,12 @@ 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.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 import io.realm.kotlin.createObject internal fun ChunkEntity.deleteOnCascade() { @@ -38,14 +40,38 @@ internal fun ChunkEntity.deleteOnCascade() { this.deleteFromRealm() } +internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, direction: PaginationDirection) { + assertIsManaged() + val eventsToMerge: List + if (direction == PaginationDirection.FORWARDS) { + this.nextToken = chunkToMerge.nextToken + this.isLastForward = chunkToMerge.isLastForward + eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + } else { + this.prevToken = chunkToMerge.prevToken + this.isLastBackward = chunkToMerge.isLastBackward + eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + } + return eventsToMerge + .forEach { + if (timelineEvents.find(it.eventId) == null) { + it.displayIndex = nextDisplayIndex(direction) + this.timelineEvents.add(it) + } + } +} + internal fun ChunkEntity.addTimelineEvent(roomId: String, eventEntity: EventEntity, direction: PaginationDirection, - roomMemberEvent: Event?): TimelineEventEntity { + roomMemberEvent: Event?) { + val eventId = eventEntity.eventId + if (timelineEvents.find(eventId) != null) { + return + } val displayIndex = nextDisplayIndex(direction) val localId = TimelineEventEntity.nextId(realm) - val eventId = eventEntity.eventId val senderId = eventEntity.sender ?: "" val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() @@ -69,24 +95,23 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, } } - val timelineEventEntity = TimelineEventEntity().also { - it.localId = localId - it.root = eventEntity - it.eventId = eventId - it.roomId = roomId - it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - it.readReceipts = readReceiptsSummaryEntity - it.displayIndex = displayIndex - } - if (roomMemberEvent != null) { - val roomMemberContent = roomMemberEvent.content.toModel() - timelineEventEntity.senderAvatar = roomMemberContent?.avatarUrl - timelineEventEntity.senderName = roomMemberContent?.displayName - timelineEventEntity.isUniqueDisplayName = false - timelineEventEntity.senderMembershipEventId = roomMemberEvent.eventId + val timelineEventEntity = realm.createObject().apply { + this.localId = localId + this.root = eventEntity + this.eventId = eventId + this.roomId = roomId + this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + this.readReceipts = readReceiptsSummaryEntity + this.displayIndex = displayIndex + if (roomMemberEvent != null) { + val roomMemberContent = roomMemberEvent.content.toModel() + this.senderAvatar = roomMemberContent?.avatarUrl + this.senderName = roomMemberContent?.displayName + this.isUniqueDisplayName = false + this.senderMembershipEventId = roomMemberEvent.eventId + } } timelineEvents.add(timelineEventEntity) - return timelineEventEntity } internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int { 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 b1aedbe21a..eb4086e1d6 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 @@ -26,9 +26,7 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, @Index var nextToken: String? = null, var timelineEvents: RealmList = RealmList(), @Index var isLastForward: Boolean = false, - @Index var isLastBackward: Boolean = false, - var backwardsDisplayIndex: Int? = null, - var forwardsDisplayIndex: Int? = null + @Index var isLastBackward: Boolean = false ) : RealmObject() { fun identifier() = "${prevToken}_$nextToken" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index cb64ecf470..834cb7f520 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -41,12 +41,6 @@ internal open class EventEntity(@PrimaryKey var eventId: String = "", var decryptionErrorCode: String? = null ) : RealmObject() { - enum class LinkFilterMode { - LINKED_ONLY, - UNLINKED_ONLY, - BOTH - } - private var sendStateStr: String = SendState.UNKNOWN.name var sendState: SendState @@ -59,9 +53,6 @@ internal open class EventEntity(@PrimaryKey var eventId: String = "", companion object - @LinkingObjects("root") - val timelineEventEntity: RealmResults? = null - fun setDecryptionResult(result: MXEventDecryptionResult) { val decryptionResult = OlmDecryptionResult( payload = result.clearEvent, @@ -72,6 +63,5 @@ internal open class EventEntity(@PrimaryKey var eventId: String = "", val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) decryptionResultJson = adapter.toJson(decryptionResult) decryptionErrorCode = null - timelineEventEntity?.firstOrNull()?.root = this } } 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 63a11b7ff9..0374f860dc 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 @@ -23,7 +23,7 @@ import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey internal open class TimelineEventEntity(var localId: Long = 0, - @PrimaryKey var eventId: String = "", + @Index var eventId: String = "", @Index var roomId: String = "", @Index var displayIndex: Int = 0, var root: EventEntity? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 590a2162f2..f8f9ab724b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -17,12 +17,10 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* import im.vector.matrix.android.internal.database.model.EventEntityFields import io.realm.Realm import io.realm.RealmList import io.realm.RealmQuery -import io.realm.Sort import io.realm.kotlin.where internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { 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 d5e3b13ac2..a182bc63d9 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 @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.model.* -import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* import io.realm.* import io.realm.kotlin.where diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 5551930bd1..c4bba4cbf6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -129,9 +129,6 @@ internal abstract class RoomModule { @Binds abstract fun bindGetContextOfEventTask(task: DefaultGetContextOfEventTask): GetContextOfEventTask - @Binds - abstract fun bindClearUnlinkedEventsTask(task: DefaultClearUnlinkedEventsTask): ClearUnlinkedEventsTask - @Binds abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask 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 26626ddfcc..bca04bbba4 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 @@ -48,21 +48,23 @@ internal class RoomSummaryUpdater @Inject constructor( private val monarchy: Monarchy) { // TODO: maybe allow user of SDK to give that list - private val PREVIEWABLE_TYPES = listOf( - EventType.MESSAGE, - EventType.STATE_ROOM_NAME, - EventType.STATE_ROOM_TOPIC, - EventType.STATE_ROOM_MEMBER, - EventType.STATE_ROOM_HISTORY_VISIBILITY, - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_ANSWER, - EventType.ENCRYPTED, - EventType.STATE_ROOM_ENCRYPTION, - EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER, - EventType.STATE_ROOM_CREATE - ) + companion object { + val PREVIEWABLE_TYPES = listOf( + EventType.MESSAGE, + EventType.STATE_ROOM_NAME, + EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_ANSWER, + EventType.ENCRYPTED, + EventType.STATE_ROOM_ENCRYPTION, + EventType.STATE_ROOM_THIRD_PARTY_INVITE, + EventType.STICKER, + EventType.STATE_ROOM_CREATE + ) + } fun update(realm: Realm, roomId: String, 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 deleted file mode 100644 index 57d53ff4a8..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - - */ -package im.vector.matrix.android.internal.session.room.timeline - -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.query.where -import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.awaitTransaction -import javax.inject.Inject - -internal interface ClearUnlinkedEventsTask : Task { - - data class Params(val roomId: String) -} - -internal class DefaultClearUnlinkedEventsTask @Inject constructor() : ClearUnlinkedEventsTask { - - override suspend fun execute(params: ClearUnlinkedEventsTask.Params) { - return - /*monarchy.awaitTransaction { localRealm -> - val unlinkedChunks = ChunkEntity - .where(localRealm, roomId = params.roomId) - .findAll() - unlinkedChunks.forEach { - it.deleteOnCascade() - } - } - */ - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index 966bdcc1fd..282de3471f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -27,8 +27,7 @@ internal interface GetContextOfEventTask : Task(eventBus) { - apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter) + // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process. + apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 1, filter) } return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) } 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 3a2415f593..a2aedf5f4d 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 @@ -71,7 +71,6 @@ internal class DefaultTimeline( private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, - private val clearUnlinkedEventsTask: ClearUnlinkedEventsTask, private val paginationTask: PaginationTask, private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, @@ -225,9 +224,6 @@ internal class DefaultTimeline( } eventDecryptor.destroy() } - clearUnlinkedEventsTask - .configureWith(ClearUnlinkedEventsTask.Params(roomId)) - .executeBy(taskExecutor) } } @@ -653,7 +649,7 @@ internal class DefaultTimeline( } private fun fetchEvent(eventId: String) { - val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize) + val params = GetContextOfEventTask.Params(roomId, eventId) cancelableBag += contextOfEventTask.configureWith(params) { callback = object : MatrixCallback { override fun onFailure(failure: Throwable) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 5ed3c76ed6..3e783f98a4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -44,8 +44,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv private val cryptoService: CryptoService, private val paginationTask: PaginationTask, private val timelineEventMapper: TimelineEventMapper, - private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - private val clearUnlinkedEventsTask: ClearUnlinkedEventsTask + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper ) : TimelineService { @AssistedInject.Factory @@ -60,7 +59,6 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv realmConfiguration = monarchy.realmConfiguration, taskExecutor = taskExecutor, contextOfEventTask = contextOfEventTask, - clearUnlinkedEventsTask = clearUnlinkedEventsTask, paginationTask = paginationTask, cryptoService = cryptoService, timelineEventMapper = timelineEventMapper, 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 1e75a52133..167d047ef3 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 @@ -22,17 +22,27 @@ 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.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addTimelineEvent +import im.vector.matrix.android.internal.database.helper.deleteOnCascade +import im.vector.matrix.android.internal.database.helper.merge 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.CurrentStateEventEntity 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.RoomSummaryEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity 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.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.getOrNull +import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.util.awaitTransaction +import io.realm.Realm +import io.realm.RealmList import io.realm.kotlin.createObject import timber.log.Timber import javax.inject.Inject @@ -117,8 +127,6 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy monarchy .awaitTransaction { realm -> Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") - val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) val nextToken: String? val prevToken: String? @@ -144,47 +152,9 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy ?: ChunkEntity.create(realm, prevToken, nextToken) if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { - Timber.v("Reach end of $roomId") - currentChunk.isLastBackward = true + handleReachEnd(realm, roomId, direction, currentChunk) } else { - Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") - val roomMemberEventsByUser = HashMap() - val eventList = if (direction == PaginationDirection.FORWARDS) { - receivedChunk.events - } else { - receivedChunk.events.asReversed() - } - val stateEvents = receivedChunk.stateEvents - for (stateEvent in stateEvents) { - if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null && !stateEvent.isRedacted()) { - roomMemberEventsByUser[stateEvent.stateKey] = stateEvent - } - } - val eventEntities = ArrayList(eventList.size) - for (event in eventList) { - if (event.eventId == null || event.senderId == null) { - continue - } - val eventEntity = event.toEntity(roomId, SendState.SYNCED).also { - realm.copyToRealmOrUpdate(it) - } - if(direction == PaginationDirection.FORWARDS){ - eventEntities.add(eventEntity) - }else { - eventEntities.add(0, eventEntity) - } - if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null && !event.isRedacted()) { - roomMemberEventsByUser[event.stateKey] = event - } - } - for (eventEntity in eventEntities) { - val senderId = eventEntity.sender ?: continue - val roomMemberEvent = roomMemberEventsByUser.getOrPut(senderId) { - CurrentStateEventEntity.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain() - } - currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberEvent) - } - roomEntity.addOrUpdate(currentChunk) + handlePagination(realm, roomId, direction, receivedChunk, currentChunk) } } return if (receivedChunk.events.isEmpty()) { @@ -197,4 +167,81 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy Result.SUCCESS } } + + private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { + Timber.v("Reach end of $roomId") + if (direction == PaginationDirection.FORWARDS) { + val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) + if (currentChunk != currentLiveChunk) { + currentChunk.isLastForward = true + currentLiveChunk?.deleteOnCascade() + RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { + latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES) + } + } + } else { + currentChunk.isLastBackward = true + } + } + + private fun handlePagination( + realm: Realm, + roomId: String, + direction: PaginationDirection, + receivedChunk: TokenChunkEvent, + currentChunk: ChunkEntity + ) { + Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") + val roomMemberEventsByUser = HashMap() + val eventList = if (direction == PaginationDirection.FORWARDS) { + receivedChunk.events + } else { + receivedChunk.events.asReversed() + } + val stateEvents = receivedChunk.stateEvents + for (stateEvent in stateEvents) { + if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null && !stateEvent.isRedacted()) { + roomMemberEventsByUser[stateEvent.stateKey] = stateEvent + } + } + val eventIds = ArrayList(eventList.size) + val eventEntities = ArrayList(eventList.size) + for (event in eventList) { + if (event.eventId == null || event.senderId == null) { + continue + } + eventIds.add(event.eventId) + val eventEntity = event.toEntity(roomId, SendState.SYNCED).let { + realm.copyToRealmOrUpdate(it) + } + if (direction == PaginationDirection.FORWARDS) { + eventEntities.add(eventEntity) + } else { + eventEntities.add(0, eventEntity) + } + if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null && !event.isRedacted()) { + roomMemberEventsByUser[event.stateKey] = event + } + } + for (eventEntity in eventEntities) { + val senderId = eventEntity.sender ?: continue + val roomMemberEvent = roomMemberEventsByUser.getOrPut(senderId) { + CurrentStateEventEntity.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain() + } + currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberEvent) + } + + val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) + val chunksToDelete = ArrayList() + chunks.forEach { + if (it != currentChunk) { + currentChunk.merge(it, direction) + chunksToDelete.add(it) + } + } + chunksToDelete.forEach { + it.deleteFromRealm() + } + RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) + } } From a8f783bbfaa16a6817cfd9d5c3be0e2d1c7166e9 Mon Sep 17 00:00:00 2001 From: Ganard Date: Tue, 28 Jan 2020 18:59:21 +0100 Subject: [PATCH 15/34] Add state events to chunks --- .../database/helper/ChunkEntityHelper.kt | 34 ++++++++++++++++--- .../internal/database/model/ChunkEntity.kt | 1 + .../room/timeline/TokenChunkEventPersistor.kt | 15 +++++--- .../internal/session/sync/RoomSyncHandler.kt | 7 ++-- 4 files changed, 44 insertions(+), 13 deletions(-) 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 5fd7e07b89..1ae381e014 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 @@ -16,12 +16,14 @@ package im.vector.matrix.android.internal.database.helper -import im.vector.matrix.android.api.session.events.model.Event 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.ChunkEntity +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields 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.EventEntityFields 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 @@ -33,6 +35,7 @@ import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import io.realm.Sort import io.realm.kotlin.createObject +import timber.log.Timber internal fun ChunkEntity.deleteOnCascade() { assertIsManaged() @@ -40,7 +43,7 @@ internal fun ChunkEntity.deleteOnCascade() { this.deleteFromRealm() } -internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, direction: PaginationDirection) { +internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) { assertIsManaged() val eventsToMerge: List if (direction == PaginationDirection.FORWARDS) { @@ -52,6 +55,9 @@ internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, direction: PaginationD this.isLastBackward = chunkToMerge.isLastBackward eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) } + chunkToMerge.stateEvents.forEach { stateEvent -> + addStateEvent(roomId, stateEvent, direction) + } return eventsToMerge .forEach { if (timelineEvents.find(it.eventId) == null) { @@ -61,10 +67,29 @@ internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, direction: PaginationD } } +internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) { + if (direction == PaginationDirection.FORWARDS) { + Timber.v("We don't keep chunk state events when paginating forward") + } else { + val stateKey = stateEvent.stateKey ?: return + val type = stateEvent.type + val pastStateEvent = stateEvents.where() + .equalTo(EventEntityFields.ROOM_ID, roomId) + .equalTo(EventEntityFields.STATE_KEY, stateKey) + .equalTo(CurrentStateEventEntityFields.TYPE, type) + .findFirst() + + if (pastStateEvent != null) { + stateEvents.remove(pastStateEvent) + } + stateEvents.add(stateEvent) + } +} + internal fun ChunkEntity.addTimelineEvent(roomId: String, eventEntity: EventEntity, direction: PaginationDirection, - roomMemberEvent: Event?) { + roomMemberEvent: EventEntity?) { val eventId = eventEntity.eventId if (timelineEvents.find(eventId) != null) { @@ -104,10 +129,9 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, this.readReceipts = readReceiptsSummaryEntity this.displayIndex = displayIndex if (roomMemberEvent != null) { - val roomMemberContent = roomMemberEvent.content.toModel() + val roomMemberContent = ContentMapper.map(roomMemberEvent.content).toModel() this.senderAvatar = roomMemberContent?.avatarUrl this.senderName = roomMemberContent?.displayName - this.isUniqueDisplayName = false this.senderMembershipEventId = roomMemberEvent.eventId } } 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 eb4086e1d6..2d294e6783 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 @@ -24,6 +24,7 @@ import io.realm.annotations.LinkingObjects internal open class ChunkEntity(@Index var prevToken: String? = null, @Index var nextToken: String? = null, + var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false 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 167d047ef3..4362f5d84d 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 @@ -21,6 +21,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.room.send.SendState import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.addStateEvent import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.helper.merge @@ -192,7 +193,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy currentChunk: ChunkEntity ) { Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") - val roomMemberEventsByUser = HashMap() + val roomMemberEventsByUser = HashMap() val eventList = if (direction == PaginationDirection.FORWARDS) { receivedChunk.events } else { @@ -200,8 +201,12 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy } val stateEvents = receivedChunk.stateEvents for (stateEvent in stateEvents) { + val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED).let { + realm.copyToRealmOrUpdate(it) + } + currentChunk.addStateEvent(roomId, stateEventEntity, direction) if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null && !stateEvent.isRedacted()) { - roomMemberEventsByUser[stateEvent.stateKey] = stateEvent + roomMemberEventsByUser[stateEvent.stateKey] = stateEventEntity } } val eventIds = ArrayList(eventList.size) @@ -220,13 +225,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy eventEntities.add(0, eventEntity) } if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null && !event.isRedacted()) { - roomMemberEventsByUser[event.stateKey] = event + roomMemberEventsByUser[event.stateKey] = eventEntity } } for (eventEntity in eventEntities) { val senderId = eventEntity.sender ?: continue val roomMemberEvent = roomMemberEventsByUser.getOrPut(senderId) { - CurrentStateEventEntity.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain() + CurrentStateEventEntity.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)?.root } currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberEvent) } @@ -235,7 +240,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy val chunksToDelete = ArrayList() chunks.forEach { if (it != currentChunk) { - currentChunk.merge(it, direction) + currentChunk.merge(roomId, it, direction) chunksToDelete.add(it) } } 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 c6f8525a42..670529074e 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 @@ -29,6 +29,7 @@ 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.CurrentStateEventEntity +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.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom @@ -210,7 +211,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle chunkEntity.isLastForward = true val eventIds = ArrayList(eventList.size) - val roomMemberEventsByUser = HashMap() + val roomMemberEventsByUser = HashMap() for (event in eventList) { if (event.eventId == null || event.senderId == null) { @@ -226,12 +227,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle root = eventEntity } if (event.type == EventType.STATE_ROOM_MEMBER) { - roomMemberEventsByUser[event.stateKey] = event + roomMemberEventsByUser[event.stateKey] = eventEntity roomMemberEventHandler.handle(realm, roomEntity.roomId, event) } } val roomMemberEvent = roomMemberEventsByUser.getOrPut(event.senderId) { - CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain() + CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root } chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberEvent) // Give info to crypto module From 7f72af426b62f78c568b637bea6731157c52e1a9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 29 Jan 2020 16:02:53 +0100 Subject: [PATCH 16/34] Timeline: fix getContext --- .../timeline/DefaultGetContextOfEventTask.kt | 2 +- .../session/room/timeline/DefaultTimeline.kt | 51 ++++++++----------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index 282de3471f..d6ec6f8ff3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -43,7 +43,7 @@ internal class DefaultGetContextOfEventTask @Inject constructor( val filter = filterRepository.getRoomFilter() val response = executeRequest(eventBus) { // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process. - apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 1, filter) + apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) } return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) } 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 a2aedf5f4d..de71378ba1 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 @@ -30,7 +30,6 @@ 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 @@ -61,7 +60,6 @@ import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.math.max -import kotlin.math.min private const val MIN_FETCHING_COUNT = 30 @@ -374,10 +372,9 @@ internal class DefaultTimeline( */ private fun paginateInternal(startDisplayIndex: Int?, direction: Timeline.Direction, - count: Int, - strict: Boolean = false): Boolean { + count: Int): Boolean { updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) } - val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong(), strict) + val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) if (shouldFetchMore) { val newRequestedCount = count - builtCount @@ -387,7 +384,6 @@ internal class DefaultTimeline( } else { updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } } - return !shouldFetchMore } @@ -451,12 +447,12 @@ internal class DefaultTimeline( if (currentInitialEventId != null && shouldFetchInitialEvent) { fetchEvent(currentInitialEventId) } else { - val count = min(settings.initialSize, filteredEvents.size) + val count = filteredEvents.size.coerceAtMost(settings.initialSize) if (initialEventId == null) { - paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false) + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) } else { - paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false) - paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2, strict = true) + paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, (count / 2).coerceAtLeast(1)) + paginateInternal(initialDisplayIndex?.minus(1), Timeline.Direction.BACKWARDS, (count / 2).coerceAtLeast(1)) } } postSnapshot() @@ -557,7 +553,7 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it access realm live results */ private fun getLiveChunk(): ChunkEntity? { - return filteredEvents.firstOrNull()?.chunk?.firstOrNull() + return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull() } /** @@ -566,13 +562,12 @@ internal class DefaultTimeline( */ private fun buildTimelineEvents(startDisplayIndex: Int?, direction: Timeline.Direction, - count: Long, - strict: Boolean = false): Int { + count: Long): Int { if (count < 1 || startDisplayIndex == null) { return 0 } val start = System.currentTimeMillis() - val offsetResults = getOffsetResults(startDisplayIndex, direction, count, strict) + val offsetResults = getOffsetResults(startDisplayIndex, direction, count) if (offsetResults.isEmpty()) { return 0 } @@ -613,23 +608,16 @@ internal class DefaultTimeline( */ private fun getOffsetResults(startDisplayIndex: Int, direction: Timeline.Direction, - count: Long, - strict: Boolean): RealmResults { + count: Long): RealmResults { val offsetQuery = filteredEvents.where() if (direction == Timeline.Direction.BACKWARDS) { - offsetQuery.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - if (strict) { - offsetQuery.lessThan(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) - } else { - offsetQuery.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) - } + offsetQuery + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) } else { - offsetQuery.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) - if (strict) { - offsetQuery.greaterThan(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) - } else { - offsetQuery.greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) - } + offsetQuery + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + .greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) } return offsetQuery .limit(count) @@ -652,6 +640,11 @@ internal class DefaultTimeline( val params = GetContextOfEventTask.Params(roomId, eventId) cancelableBag += contextOfEventTask.configureWith(params) { callback = object : MatrixCallback { + + override fun onSuccess(data: TokenChunkEventPersistor.Result) { + postSnapshot() + } + override fun onFailure(failure: Throwable) { postFailure(failure) } @@ -697,7 +690,7 @@ internal class DefaultTimeline( forwardsState.set(State()) } - // Extension methods *************************************************************************** +// Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS From 71a02a58af2b5643d956af8fe336b0617dae5a47 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 29 Jan 2020 17:30:31 +0100 Subject: [PATCH 17/34] Sync/Timeline: handle displayName isUnique --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 6 +-- .../api/session/room/state/StateService.kt | 4 +- .../database/helper/ChunkEntityHelper.kt | 19 +++---- .../database/helper/RoomEntityHelper.kt | 24 --------- .../query/CurrentStateEventEntityQueries.kt | 12 +++-- .../membership/RoomDisplayNameResolver.kt | 2 +- .../room/membership/RoomMemberHelper.kt | 13 +++-- .../session/room/send/LocalEchoRepository.kt | 2 +- .../session/room/state/DefaultStateService.kt | 10 ++-- .../room/timeline/TokenChunkEventPersistor.kt | 52 +++++++++---------- .../internal/session/sync/RoomSyncHandler.kt | 33 ++++++++---- .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../RoomMemberProfileViewModel.kt | 2 +- .../members/RoomMemberListViewModel.kt | 2 +- 14 files changed, 89 insertions(+), 94 deletions(-) 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 ef55b090b7..be74b9f21b 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 @@ -61,10 +61,10 @@ class RxRoom(private val room: Room) { } } - fun liveStateEvent(eventType: String): Observable> { - return room.getStateEventLive(eventType).asObservable() + fun liveStateEvent(eventType: String, stateKey: String): Observable> { + return room.getStateEventLive(eventType, stateKey).asObservable() .startWithCallable { - room.getStateEvent(eventType).toOptional() + room.getStateEvent(eventType, stateKey).toOptional() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 55bf816b7a..f7556f3ce2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -33,7 +33,7 @@ interface StateService { */ fun enableEncryption(algorithm: String, callback: MatrixCallback) - fun getStateEvent(eventType: String): Event? + fun getStateEvent(eventType: String, stateKey: String): Event? - fun getStateEventLive(eventType: String): LiveData> + fun getStateEventLive(eventType: String, stateKey: String): LiveData> } 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 1ae381e014..bccbea93d5 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 @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.database.helper -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.ChunkEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity @@ -89,7 +87,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, internal fun ChunkEntity.addTimelineEvent(roomId: String, eventEntity: EventEntity, direction: PaginationDirection, - roomMemberEvent: EventEntity?) { + roomMemberContentsByUser: HashMap) { val eventId = eventEntity.eventId if (timelineEvents.find(eventId) != null) { @@ -128,12 +126,15 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() this.readReceipts = readReceiptsSummaryEntity this.displayIndex = displayIndex - if (roomMemberEvent != null) { - val roomMemberContent = ContentMapper.map(roomMemberEvent.content).toModel() - this.senderAvatar = roomMemberContent?.avatarUrl - this.senderName = roomMemberContent?.displayName - this.senderMembershipEventId = roomMemberEvent.eventId - } + + val roomMemberContent = roomMemberContentsByUser[senderId] + val isUnique = roomMemberContentsByUser.values.find { + roomMemberContent != it && + it?.displayName == roomMemberContent?.displayName + } == null + this.senderAvatar = roomMemberContent?.avatarUrl + this.senderName = roomMemberContent?.displayName + this.isUniqueDisplayName = isUnique } timelineEvents.add(timelineEventEntity) } 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 aa3490d2c4..81e4d30abe 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 @@ -16,14 +16,8 @@ package im.vector.matrix.android.internal.database.helper -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.send.SendState -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.RoomEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper -import io.realm.Realm internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { chunks.remove(chunkEntity) @@ -35,21 +29,3 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) { chunks.add(chunkEntity) } } - -internal fun RoomEntity.addSendingEvent(realm: Realm, event: Event) { - val senderId = event.senderId ?: return - val eventEntity = event.toEntity(roomId, SendState.UNSENT) - val roomMembers = RoomMemberHelper(realm, roomId) - val myUser = roomMembers.getLastRoomMember(senderId) - val localId = TimelineEventEntity.nextId(realm) - val timelineEventEntity = TimelineEventEntity(localId).also { - it.root = eventEntity - it.eventId = event.eventId ?: "" - it.roomId = roomId - it.senderName = myUser?.displayName - it.senderAvatar = myUser?.avatarUrl - it.isUniqueDisplayName = roomMembers.isUniqueDisplayName() - } - sendingTimelineEvents.add(0, timelineEventEntity) -} - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt index 25dec57e46..0ca24b5ece 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt @@ -23,19 +23,23 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.createObject -internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: String, stateKey: String, type: String): RealmQuery { +internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: String, type: String): RealmQuery { return realm.where(CurrentStateEventEntity::class.java) .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) - .equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey) .equalTo(CurrentStateEventEntityFields.TYPE, type) } +internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String): RealmQuery { + return where(realm = realm, roomId = roomId, type = type) + .equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey) +} + internal fun CurrentStateEventEntity.Companion.getOrNull(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity? { - return where(realm, roomId, stateKey, type).findFirst() + return whereStateKey(realm = realm, roomId = roomId, type = type, stateKey = stateKey).findFirst() } internal fun CurrentStateEventEntity.Companion.getOrCreate(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { - return getOrNull(realm, roomId, stateKey, type) ?: create(realm, roomId, stateKey, type) + return getOrNull(realm = realm, roomId = roomId, stateKey = stateKey, type = type) ?: create(realm, roomId, stateKey, type) } private fun create(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { 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 4056de1052..feb05a3275 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 @@ -129,7 +129,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, roomMemberHelper: RoomMemberHelper): String? { if (roomMemberSummary == null) return null - val isUnique = roomMemberHelper.isUniqueDisplayName() + val isUnique = roomMemberHelper.isUniqueDisplayName(roomMemberSummary.displayName) return if (isUnique) { roomMemberSummary.displayName } else { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt index 094b4d2530..05f6fdff23 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt @@ -18,9 +18,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.room.model.Membership -import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.where @@ -50,8 +51,14 @@ internal class RoomMemberHelper(private val realm: Realm, .findFirst() } - fun isUniqueDisplayName(): Boolean { - return false + fun isUniqueDisplayName(displayName: String?): Boolean { + if (displayName.isNullOrEmpty()) { + return true + } + return RoomMemberSummaryEntity.where(realm, roomId) + .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, displayName) + .findAll() + .size == 1 } fun queryRoomMembersEvent(): RealmQuery { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt index ab014596b0..a78ae3cdec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -62,7 +62,7 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon it.roomId = roomId it.senderName = myUser?.displayName it.senderAvatar = myUser?.avatarUrl - it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName() + it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName) } roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomSummaryUpdater.update(realm, roomId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 9b027ede6f..efa698d3d4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -31,7 +31,7 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.query.getOrNull -import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.query.whereStateKey import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.Realm @@ -48,15 +48,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private fun create(roomId: String): StateService } - override fun getStateEvent(eventType: String): Event? { + override fun getStateEvent(eventType: String, stateKey: String): Event? { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - CurrentStateEventEntity.getOrNull(realm, roomId, type = eventType, stateKey = "")?.root?.asDomain() + CurrentStateEventEntity.getOrNull(realm, roomId, type = eventType, stateKey = stateKey)?.root?.asDomain() } } - override fun getStateEventLive(eventType: String): LiveData> { + override fun getStateEventLive(eventType: String, stateKey: String): LiveData> { val liveData = monarchy.findAllMappedWithChanges( - { realm -> CurrentStateEventEntity.where(realm, roomId, type = eventType, stateKey = "") }, + { realm -> CurrentStateEventEntity.whereStateKey(realm, roomId, type = eventType, stateKey = "") }, { it.root?.asDomain() } ) return Transformations.map(liveData) { results -> 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 4362f5d84d..45b1cbc471 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 @@ -17,19 +17,20 @@ package im.vector.matrix.android.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy -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.RoomMemberContent import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addStateEvent import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.helper.merge -import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.mapper.ContentMapper 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.CurrentStateEventEntity -import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields 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.TimelineEventEntity @@ -37,14 +38,11 @@ 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.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm -import io.realm.RealmList -import io.realm.kotlin.createObject import timber.log.Timber import javax.inject.Inject @@ -193,24 +191,29 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy currentChunk: ChunkEntity ) { Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") - val roomMemberEventsByUser = HashMap() - val eventList = if (direction == PaginationDirection.FORWARDS) { - receivedChunk.events - } else { - receivedChunk.events.asReversed() - } + val roomMemberContentsByUser = HashMap() + val eventList = receivedChunk.events val stateEvents = receivedChunk.stateEvents + + realm.where(CurrentStateEventEntity::class.java) + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) + .findAll() + .forEach { + val roomMember = ContentMapper.map(it.root?.content).toModel() + roomMemberContentsByUser[it.stateKey] = roomMember + } + for (stateEvent in stateEvents) { val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED).let { realm.copyToRealmOrUpdate(it) } currentChunk.addStateEvent(roomId, stateEventEntity, direction) if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null && !stateEvent.isRedacted()) { - roomMemberEventsByUser[stateEvent.stateKey] = stateEventEntity + roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() } } val eventIds = ArrayList(eventList.size) - val eventEntities = ArrayList(eventList.size) for (event in eventList) { if (event.eventId == null || event.senderId == null) { continue @@ -219,23 +222,16 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy val eventEntity = event.toEntity(roomId, SendState.SYNCED).let { realm.copyToRealmOrUpdate(it) } - if (direction == PaginationDirection.FORWARDS) { - eventEntities.add(eventEntity) - } else { - eventEntities.add(0, eventEntity) - } if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null && !event.isRedacted()) { - roomMemberEventsByUser[event.stateKey] = eventEntity + val contentToUse = if (direction == PaginationDirection.FORWARDS) { + event.content + } else { + event.prevContent + } + roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() } + currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) } - for (eventEntity in eventEntities) { - val senderId = eventEntity.sender ?: continue - val roomMemberEvent = roomMemberEventsByUser.getOrPut(senderId) { - CurrentStateEventEntity.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)?.root - } - currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberEvent) - } - val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) val chunksToDelete = ArrayList() chunks.forEach { 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 670529074e..9cf91ed28b 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 @@ -21,20 +21,21 @@ 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.api.session.room.model.tag.RoomTagContent import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.crypto.DefaultCryptoService -import im.vector.matrix.android.internal.database.helper.* -import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.addTimelineEvent +import im.vector.matrix.android.internal.database.mapper.ContentMapper 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.CurrentStateEventEntity -import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity 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.getOrCreate -import im.vector.matrix.android.internal.database.query.getOrNull 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 @@ -44,7 +45,11 @@ import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.typing.TypingEventContent -import im.vector.matrix.android.internal.session.sync.model.* +import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync +import im.vector.matrix.android.internal.session.sync.model.RoomSync +import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData +import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral +import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse import io.realm.Realm import io.realm.kotlin.createObject import org.greenrobot.eventbus.EventBus @@ -211,7 +216,16 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle chunkEntity.isLastForward = true val eventIds = ArrayList(eventList.size) - val roomMemberEventsByUser = HashMap() + val roomMemberContentsByUser = HashMap() + + realm.where(CurrentStateEventEntity::class.java) + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) + .findAll() + .forEach { + val roomMember = ContentMapper.map(it.root?.content).toModel() + roomMemberContentsByUser[it.stateKey] = roomMember + } for (event in eventList) { if (event.eventId == null || event.senderId == null) { @@ -227,14 +241,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle root = eventEntity } if (event.type == EventType.STATE_ROOM_MEMBER) { - roomMemberEventsByUser[event.stateKey] = eventEntity + roomMemberContentsByUser[event.stateKey] = event.content.toModel() roomMemberEventHandler.handle(realm, roomEntity.roomId, event) } } - val roomMemberEvent = roomMemberEventsByUser.getOrPut(event.senderId) { - CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root - } - chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberEvent) + chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) // Give info to crypto module cryptoService.onLiveEvent(roomEntity.roomId, event) // Try to remove local echo 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 70f6745871..60443f6412 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 @@ -901,7 +901,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro setState { copy(asyncInviter = Success(it)) } } } - room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE)?.also { + room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, "")?.also { setState { copy(tombstoneEvent = it) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt index 88f394e865..e4a15724b4 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -147,7 +147,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v private fun observeRoomSummaryAndPowerLevels(room: Room) { val roomSummaryLive = room.rx().liveRoomSummary().unwrap() - val powerLevelsContentLive = room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + val powerLevelsContentLive = room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "") .mapOptional { it.content.toModel() } .unwrap() diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt index 42ef8f285d..c27afaceb0 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt @@ -72,7 +72,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState .combineLatest, PowerLevelsContent, RoomMemberSummaries>( room.rx().liveRoomMembers(roomMemberQueryParams), room.rx() - .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "") .mapOptional { it.content.toModel() } .unwrap(), BiFunction { roomMembers, powerLevelsContent -> From 9fc3fa7f19af5df2d370e55ec8bd0c3ffdc68265 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 29 Jan 2020 21:14:38 +0100 Subject: [PATCH 18/34] Update some libs and remove incremental from dagger --- build.gradle | 12 ------------ matrix-sdk-android/build.gradle | 11 ++++++----- vector/build.gradle | 4 ++-- .../vector/riotx/core/platform/VectorBaseActivity.kt | 3 +-- .../platform/VectorBaseBottomSheetDialogFragment.kt | 5 ++--- .../vector/riotx/core/platform/VectorBaseFragment.kt | 4 ++-- 6 files changed, 13 insertions(+), 26 deletions(-) diff --git a/build.gradle b/build.gradle index 29351e403f..a2fac55175 100644 --- a/build.gradle +++ b/build.gradle @@ -47,23 +47,11 @@ allprojects { jcenter() } - tasks.withType(JavaCompile).all { - options.compilerArgs += [ - '-Adagger.gradle.incremental=enabled' - ] - } - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { // Warnings are potential errors, so stop ignoring them kotlinOptions.allWarningsAsErrors = true } - afterEvaluate { - extensions.findByName("kapt")?.arguments { - arg("dagger.gradle.incremental", "enabled") - } - } - } task clean(type: Delete) { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index b902108680..529737be37 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -92,10 +92,11 @@ dependencies { def arrow_version = "0.8.2" def moshi_version = '1.8.0' - def lifecycle_version = '2.1.0' + def lifecycle_version = '2.2.0' + def arch_version = '2.1.0' def coroutines_version = "1.3.2" def markwon_version = '3.1.0' - def daggerVersion = '2.24' + def daggerVersion = '2.25.4' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" @@ -104,7 +105,7 @@ dependencies { implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" - kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" // Network implementation 'com.squareup.retrofit2:retrofit:2.6.2' @@ -124,7 +125,7 @@ dependencies { kapt 'dk.ilios:realmfieldnameshelper:1.1.1' // Work - implementation "androidx.work:work-runtime-ktx:2.3.0-beta02" + implementation "androidx.work:work-runtime-ktx:2.3.0" // FP implementation "io.arrow-kt:arrow-core:$arrow_version" @@ -166,7 +167,7 @@ dependencies { androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 androidTestImplementation 'io.mockk:mockk-android:1.9.2.kotlin12' - androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version" + androidTestImplementation "androidx.arch.core:core-testing:$arch_version" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" } diff --git a/vector/build.gradle b/vector/build.gradle index 14ec9f2c21..ac7fa3f735 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -223,14 +223,14 @@ android { dependencies { def epoxy_version = '3.9.0' - def fragment_version = '1.2.0-rc04' + def fragment_version = '1.2.0' def arrow_version = "0.8.2" def coroutines_version = "1.3.2" def markwon_version = '4.1.2' def big_image_viewer_version = '1.6.2' def glide_version = '4.10.0' def moshi_version = '1.8.0' - def daggerVersion = '2.24' + def daggerVersion = '2.25.4' def autofill_version = "1.0.0" implementation project(":matrix-sdk-android") 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 5c73fc97da..d57b11d93d 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 @@ -38,7 +38,6 @@ import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders import butterknife.BindView import butterknife.ButterKnife import butterknife.Unbinder @@ -92,7 +91,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { private lateinit var viewModelFactory: ViewModelProvider.Factory protected val viewModelProvider - get() = ViewModelProviders.of(this, viewModelFactory) + get() = ViewModelProvider(this, viewModelFactory) /* ========================================================================================== * DATA diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt index b3a56f48ee..b24c685dba 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -22,7 +22,6 @@ import android.os.Parcelable import android.widget.FrameLayout import androidx.annotation.CallSuper import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRxView import com.airbnb.mvrx.MvRxViewId @@ -50,10 +49,10 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() private lateinit var viewModelFactory: ViewModelProvider.Factory protected val activityViewModelProvider - get() = ViewModelProviders.of(requireActivity(), viewModelFactory) + get() = ViewModelProvider(requireActivity(), viewModelFactory) protected val fragmentViewModelProvider - get() = ViewModelProviders.of(this, viewModelFactory) + get() = ViewModelProvider(this, viewModelFactory) /* ========================================================================================== * BottomSheetBehavior 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 91c166a96c..93a48ed925 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 @@ -71,10 +71,10 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { private lateinit var viewModelFactory: ViewModelProvider.Factory protected val activityViewModelProvider - get() = ViewModelProviders.of(requireActivity(), viewModelFactory) + get() = ViewModelProvider(requireActivity(), viewModelFactory) protected val fragmentViewModelProvider - get() = ViewModelProviders.of(this, viewModelFactory) + get() = ViewModelProvider(this, viewModelFactory) /* ========================================================================================== * Life cycle From 5e1b59f9d373dc0d52fe6297f7d5aa7f015b88ad Mon Sep 17 00:00:00 2001 From: Ganard Date: Thu, 30 Jan 2020 16:20:41 +0100 Subject: [PATCH 19/34] Timeline: handle an in memory local echo to make the UI snappier --- .../matrix/android/common/CommonTestHelper.kt | 4 +++ .../api/session/room/timeline/Timeline.kt | 2 +- .../session/room/timeline/TimelineEvent.kt | 1 + .../database/mapper/TimelineEventMapper.kt | 1 + .../room/send/LocalEchoEventFactory.kt | 3 +- .../session/room/send/LocalEchoRepository.kt | 28 +++++++++++++----- .../session/room/timeline/DefaultTimeline.kt | 29 ++++++++++++++++--- .../room/detail/ScrollOnNewMessageCallback.kt | 18 +++++++----- .../timeline/TimelineEventController.kt | 3 +- 9 files changed, 66 insertions(+), 23 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index b16c865765..f348a8b75c 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -106,6 +106,10 @@ class CommonTestHelper(context: Context) { override fun onTimelineFailure(throwable: Throwable) { } + override fun onNewTimelineEvents(eventIds: List) { + //noop + } + override fun onTimelineUpdated(snapshot: List) { // TODO Count only new messages? if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) { 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 16bf522c59..164afb3a60 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 @@ -114,7 +114,7 @@ interface Timeline { fun onTimelineFailure(throwable: Throwable) /** - * Call when new events come through the sync + * Called when new events come through the sync */ fun onNewTimelineEvents(eventIds: List) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index caa64a85f8..0c8a04db36 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventConten data class TimelineEvent( val root: Event, val localId: Long, + val eventId: String, val displayIndex: Int, val senderName: String?, val isUniqueDisplayName: Boolean, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index aa282fb673..4bd9b9855b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -37,6 +37,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), + eventId = timelineEventEntity.eventId, annotations = timelineEventEntity.annotations?.asDomain(), localId = timelineEventEntity.localId, displayIndex = timelineEventEntity.displayIndex, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 383c961aaf..0db7377d4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -56,6 +56,7 @@ import im.vector.matrix.android.internal.util.StringProvider import kotlinx.coroutines.launch import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer +import timber.log.Timber import javax.inject.Inject /** @@ -419,7 +420,7 @@ internal class LocalEchoEventFactory @Inject constructor( ) } - fun createLocalEcho(event: Event){ + fun createLocalEcho(event: Event) { checkNotNull(event.roomId) { "Your event should have a roomId" } taskExecutor.executorScope.launch { localEchoRepository.createLocalEcho(event) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt index a78ae3cdec..bc0f413e92 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -24,13 +24,17 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.helper.nextId +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.mapper.toEntity +import im.vector.matrix.android.internal.database.model.ChunkEntity 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.query.findAllInRoomWithSendStates +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.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper @@ -39,24 +43,27 @@ import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import org.greenrobot.eventbus.EventBus import timber.log.Timber +import java.lang.IllegalStateException import javax.inject.Inject +import kotlin.random.Random internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy, private val roomSummaryUpdater: RoomSummaryUpdater, - private val eventBus: EventBus) { + private val eventBus: EventBus, + private val timelineEventMapper: TimelineEventMapper) { suspend fun createLocalEcho(event: Event) { - val roomId = event.roomId ?: return - val senderId = event.senderId ?: return - val eventId = event.eventId ?: return - eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = listOf(eventId))) - monarchy.awaitTransaction { realm -> - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction + val roomId = event.roomId ?: throw IllegalStateException("You should have set a roomId for your event") + val senderId = event.senderId ?: throw IllegalStateException("You should have set a senderIf for your event") + if (event.eventId == null) { + throw IllegalStateException("You should have set an eventId for your event") + } + val timelineEventEntity = Realm.getInstance(monarchy.realmConfiguration).use { realm -> val eventEntity = event.toEntity(roomId, SendState.UNSENT) val roomMemberHelper = RoomMemberHelper(realm, roomId) val myUser = roomMemberHelper.getLastRoomMember(senderId) val localId = TimelineEventEntity.nextId(realm) - val timelineEventEntity = TimelineEventEntity(localId).also { + TimelineEventEntity(localId).also { it.root = eventEntity it.eventId = event.eventId it.roomId = roomId @@ -64,6 +71,11 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon it.senderAvatar = myUser?.avatarUrl it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName) } + } + val timelineEvent = timelineEventMapper.map(timelineEventEntity) + eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)) + monarchy.awaitTransaction { realm -> + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomSummaryUpdater.update(realm, roomId) } 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 de71378ba1..b69b25d09a 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 @@ -78,6 +78,7 @@ internal class DefaultTimeline( ) : Timeline, TimelineHiddenReadReceipts.Delegate { data class OnNewTimelineEvents(val roomId: String, val eventIds: List) + data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent) companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -99,6 +100,7 @@ internal class DefaultTimeline( private var prevDisplayIndex: Int? = null private var nextDisplayIndex: Int? = null + private val inMemorySendingEvents = Collections.synchronizedList(ArrayList()) private val builtEvents = Collections.synchronizedList(ArrayList()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsState = AtomicReference(State()) @@ -321,13 +323,24 @@ internal class DefaultTimeline( @Subscribe(threadMode = ThreadMode.MAIN) fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) { - if (onNewTimelineEvents.roomId == roomId) { + if (isLive && onNewTimelineEvents.roomId == roomId) { listeners.forEach { it.onNewTimelineEvents(onNewTimelineEvents.eventIds) } } } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) { + if (isLive && onLocalEchoCreated.roomId == roomId) { + listeners.forEach { + it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId)) + } + inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent) + postSnapshot() + } + } + // Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { @@ -394,12 +407,15 @@ internal class DefaultTimeline( private fun buildSendingEvents(): List { val sendingEvents = ArrayList() if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { + sendingEvents.addAll(inMemorySendingEvents) roomEntity?.sendingTimelineEvents ?.where() ?.filterEventsWithSettings() ?.findAll() - ?.forEach { - sendingEvents.add(timelineEventMapper.map(it)) + ?.forEach { timelineEventEntity -> + if (sendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { + sendingEvents.add(timelineEventMapper.map(timelineEventEntity)) + } } } return sendingEvents @@ -580,6 +596,11 @@ internal class DefaultTimeline( offsetResults.forEach { eventEntity -> val timelineEvent = buildTimelineEvent(eventEntity) + val transactionId = timelineEvent.root.unsignedData?.transactionId + val sendingEvent = inMemorySendingEvents.find { + it.eventId == transactionId + } + inMemorySendingEvents.remove(sendingEvent) if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { @@ -665,7 +686,7 @@ internal class DefaultTimeline( it.onTimelineUpdated(snapshot) } } - debouncer.debounce("post_snapshot", runnable, 50) + debouncer.debounce("post_snapshot", runnable, 1) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt index 2180571c3b..6a1646f009 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -21,26 +21,30 @@ import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem import timber.log.Timber +import java.util.concurrent.CopyOnWriteArrayList class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { - private val newTimelineEventIds = HashSet() + private val newTimelineEventIds = CopyOnWriteArrayList() - fun addNewTimelineEventIds(eventIds: List){ - newTimelineEventIds.addAll(eventIds) + fun addNewTimelineEventIds(eventIds: List) { + newTimelineEventIds.addAll(0, eventIds) } override fun onInserted(position: Int, count: Int) { Timber.v("On inserted $count count at position: $position") - if(layoutManager.findFirstVisibleItemPosition() != position ){ + if (layoutManager.findFirstVisibleItemPosition() != position) { return } val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return - val firstNewItemIds = firstNewItem.getEventIds() - if(newTimelineEventIds.intersect(firstNewItemIds).isNotEmpty()){ + val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() + val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds) + if (indexOfFirstNewItem != -1) { Timber.v("Should scroll to position: $position") - newTimelineEventIds.clear() + repeat(newTimelineEventIds.size - indexOfFirstNewItem) { + newTimelineEventIds.removeAt(indexOfFirstNewItem) + } layoutManager.scrollToPosition(position) } } 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 1579a77779..cdc75477fb 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 @@ -30,7 +30,6 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ -import im.vector.riotx.core.epoxy.emptyItem import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.UnreadState @@ -253,7 +252,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec currentSnapshot = newSnapshot val diffResult = DiffUtil.calculateDiff(diffCallback) diffResult.dispatchUpdatesTo(listUpdateCallback) - requestDelayedModelBuild(100) + requestModelBuild() inSubmitList = false } } From 759b680e6344f065f34e003910df36622449a55c Mon Sep 17 00:00:00 2001 From: Ganard Date: Thu, 30 Jan 2020 18:44:49 +0100 Subject: [PATCH 20/34] Timeline/Sync: Fix some issues --- .../database/helper/ChunkEntityHelper.kt | 97 +++++++++++++------ .../database/query/EventEntityQueries.kt | 8 +- .../query/TimelineEventEntityQueries.kt | 11 --- .../room/EventRelationsAggregationUpdater.kt | 4 +- .../create/RoomCreateEventLiveObserver.kt | 4 +- .../session/room/prune/EventsPruner.kt | 4 +- .../session/room/prune/PruneEventTask.kt | 8 +- .../timeline/DefaultGetContextOfEventTask.kt | 3 +- .../room/timeline/TokenChunkEventPersistor.kt | 30 +++--- .../RoomTombstoneEventLiveObserver.kt | 4 +- .../internal/session/sync/RoomSyncHandler.kt | 18 ++-- 11 files changed, 109 insertions(+), 82 deletions(-) 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 bccbea93d5..16ee544f4b 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 @@ -24,6 +24,8 @@ 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.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields 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.query.find @@ -31,8 +33,10 @@ 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.Realm import io.realm.Sort import io.realm.kotlin.createObject +import kotlinx.coroutines.coroutineScope import timber.log.Timber internal fun ChunkEntity.deleteOnCascade() { @@ -43,6 +47,7 @@ internal fun ChunkEntity.deleteOnCascade() { internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) { assertIsManaged() + val localRealm = this.realm val eventsToMerge: List if (direction == PaginationDirection.FORWARDS) { this.nextToken = chunkToMerge.nextToken @@ -59,15 +64,32 @@ internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direct return eventsToMerge .forEach { if (timelineEvents.find(it.eventId) == null) { - it.displayIndex = nextDisplayIndex(direction) - this.timelineEvents.add(it) + val eventId = it.eventId + if (timelineEvents.find(eventId) != null) { + return + } + val displayIndex = nextDisplayIndex(direction) + val localId = TimelineEventEntity.nextId(realm) + val copied = localRealm.createObject().apply { + this.localId = localId + this.root = it.root + this.eventId = it.eventId + this.roomId = it.roomId + this.annotations = it.annotations + this.readReceipts = it.readReceipts + this.displayIndex = displayIndex + this.senderAvatar = it.senderAvatar + this.senderName = it.senderName + this.isUniqueDisplayName = it.isUniqueDisplayName + } + timelineEvents.add(copied) } } } internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) { - if (direction == PaginationDirection.FORWARDS) { - Timber.v("We don't keep chunk state events when paginating forward") + if (direction == PaginationDirection.BACKWARDS) { + Timber.v("We don't keep chunk state events when paginating backward") } else { val stateKey = stateEvent.stateKey ?: return val type = stateEvent.type @@ -97,27 +119,8 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, val localId = TimelineEventEntity.nextId(realm) val senderId = eventEntity.sender ?: "" - val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: realm.createObject(eventId).apply { - this.roomId = roomId - } - // Update RR for the sender of a new message with a dummy one - - val originServerTs = eventEntity.originServerTs - if (originServerTs != null) { - val timestampOfEvent = originServerTs.toDouble() - val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) - // If the synced RR is older, update - if (timestampOfEvent > readReceiptOfSender.originServerTs) { - val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() - readReceiptOfSender.eventId = eventId - readReceiptOfSender.originServerTs = timestampOfEvent - previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender) - readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender) - } - } - + val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId) val timelineEventEntity = realm.createObject().apply { this.localId = localId this.root = eventEntity @@ -126,19 +129,53 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() this.readReceipts = readReceiptsSummaryEntity this.displayIndex = displayIndex - val roomMemberContent = roomMemberContentsByUser[senderId] - val isUnique = roomMemberContentsByUser.values.find { - roomMemberContent != it && - it?.displayName == roomMemberContent?.displayName - } == null this.senderAvatar = roomMemberContent?.avatarUrl this.senderName = roomMemberContent?.displayName - this.isUniqueDisplayName = isUnique + if (roomMemberContent?.displayName != null) { + val isHistoricalUnique = roomMemberContentsByUser.values.find { + roomMemberContent != it && + it?.displayName == roomMemberContent.displayName + } == null + isUniqueDisplayName = if (isLastForward) { + val isLiveUnique = RoomMemberSummaryEntity + .where(realm, roomId) + .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, roomMemberContent.displayName) + .findAll().none { + !roomMemberContentsByUser.containsKey(it.userId) + } + isHistoricalUnique && isLiveUnique + } else { + isHistoricalUnique + } + } else { + isUniqueDisplayName = true + } } timelineEvents.add(timelineEventEntity) } +private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { + val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst() + ?: realm.createObject(eventEntity.eventId).apply { + this.roomId = roomId + } + val originServerTs = eventEntity.originServerTs + if (originServerTs != null) { + val timestampOfEvent = originServerTs.toDouble() + val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) + // If the synced RR is older, update + if (timestampOfEvent > readReceiptOfSender.originServerTs) { + val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() + readReceiptOfSender.eventId = eventEntity.eventId + readReceiptOfSender.originServerTs = timestampOfEvent + previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender) + readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender) + } + } + return readReceiptsSummaryEntity +} + internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int { return when (direction) { PaginationDirection.FORWARDS -> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index f8f9ab724b..59908aa990 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -44,10 +44,14 @@ internal fun EventEntity.Companion.whereType(realm: Realm, return query.equalTo(EventEntityFields.TYPE, type) } -internal fun EventEntity.Companion.types(realm: Realm, - typeList: List = emptyList()): RealmQuery { +internal fun EventEntity.Companion.whereTypes(realm: Realm, + typeList: List = emptyList(), + roomId: String? = null): RealmQuery { val query = realm.where() query.`in`(EventEntityFields.TYPE, typeList.toTypedArray()) + if (roomId != null) { + query.equalTo(EventEntityFields.ROOM_ID, roomId) + } 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 a182bc63d9..a2382d5cae 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 @@ -38,17 +38,6 @@ internal fun TimelineEventEntity.Companion.whereRoomId(realm: Realm, return realm.where().equalTo(TimelineEventEntityFields.ROOM_ID, roomId) } -internal fun TimelineEventEntity.Companion.whereType(realm: Realm, - type: String, - roomId: String? = null): RealmQuery { - val query = realm.where() - query.equalTo(TimelineEventEntityFields.ROOT.TYPE, type) - if (roomId != null) { - query.equalTo(TimelineEventEntityFields.ROOM_ID, roomId) - } - return query -} - internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List { return realm.where() .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId) 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 c6bcdf396e..3aaa60680b 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 @@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.RealmLiveEntityObserver 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.database.query.whereTypes import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.UserId import io.realm.OrderedCollectionChangeSet @@ -43,7 +43,7 @@ internal class EventRelationsAggregationUpdater @Inject constructor( RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { - EventEntity.types(it, listOf( + EventEntity.whereTypes(it, listOf( EventType.MESSAGE, EventType.REDACTION, EventType.REACTION, 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 1553ddec04..e71fe8d0d1 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 @@ -27,7 +27,7 @@ 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 -import im.vector.matrix.android.internal.database.query.types +import im.vector.matrix.android.internal.database.query.whereTypes import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import io.realm.OrderedCollectionChangeSet @@ -41,7 +41,7 @@ internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { - EventEntity.types(it, listOf(EventType.STATE_ROOM_CREATE)) + EventEntity.whereTypes(it, listOf(EventType.STATE_ROOM_CREATE)) } override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { 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 b29d3210bc..27e00c75ab 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 @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.RealmLiveEntityObserver 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.database.query.whereTypes import im.vector.matrix.android.internal.di.SessionDatabase import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration @@ -38,7 +38,7 @@ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfigurat private val pruneEventTask: PruneEventTask) : RealmLiveEntityObserver(realmConfiguration) { - override val query = Monarchy.Query { EventEntity.types(it, listOf(EventType.REDACTION)) } + override val query = Monarchy.Query { EventEntity.whereTypes(it, listOf(EventType.REDACTION)) } override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { Timber.v("Event pruner called with ${changeSet.insertions.size} insertions") 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 89b5ae5a83..d389a0779c 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 @@ -95,10 +95,12 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M // } } } - // TODO : make it work again. Maybe waits for SQL rework... if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) { - TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId) - + TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId).forEach { + it.senderName = null + it.isUniqueDisplayName = false + it.senderAvatar = null + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index d6ec6f8ff3..85036f8e2e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -38,13 +38,12 @@ internal class DefaultGetContextOfEventTask @Inject constructor( private val eventBus: EventBus ) : GetContextOfEventTask { - override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { val filter = filterRepository.getRoomFilter() val response = executeRequest(eventBus) { // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process. apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) } - return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) + return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.FORWARDS) } } 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 45b1cbc471..f2439d57ca 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 @@ -38,6 +38,8 @@ 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.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.getOrCreate +import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater @@ -195,21 +197,12 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy val eventList = receivedChunk.events val stateEvents = receivedChunk.stateEvents - realm.where(CurrentStateEventEntity::class.java) - .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) - .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) - .findAll() - .forEach { - val roomMember = ContentMapper.map(it.root?.content).toModel() - roomMemberContentsByUser[it.stateKey] = roomMember - } - for (stateEvent in stateEvents) { val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED).let { realm.copyToRealmOrUpdate(it) } currentChunk.addStateEvent(roomId, stateEventEntity, direction) - if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null && !stateEvent.isRedacted()) { + if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) { roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() } } @@ -222,14 +215,15 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy val eventEntity = event.toEntity(roomId, SendState.SYNCED).let { realm.copyToRealmOrUpdate(it) } - if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null && !event.isRedacted()) { - val contentToUse = if (direction == PaginationDirection.FORWARDS) { - event.content - } else { + if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { + val contentToUse = if (direction == PaginationDirection.BACKWARDS) { event.prevContent + } else { + event.content } roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() } + currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) } val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) @@ -240,8 +234,14 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy chunksToDelete.add(it) } } + val shouldUpdateSummary = chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS chunksToDelete.forEach { - it.deleteFromRealm() + it.deleteOnCascade() + } + if (shouldUpdateSummary) { + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES) + roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent } RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) } 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 e5e538ae89..12f7f92794 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 @@ -27,7 +27,7 @@ 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 -import im.vector.matrix.android.internal.database.query.types +import im.vector.matrix.android.internal.database.query.whereTypes import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import io.realm.OrderedCollectionChangeSet @@ -41,7 +41,7 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { - EventEntity.types(it, listOf(EventType.STATE_ROOM_TOMBSTONE)) + EventEntity.whereTypes(it, listOf(EventType.STATE_ROOM_TOMBSTONE)) } override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { 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 9cf91ed28b..b8ac0318fd 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 @@ -31,11 +31,11 @@ import im.vector.matrix.android.internal.database.mapper.ContentMapper 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.CurrentStateEventEntity -import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity 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.getOrCreate +import im.vector.matrix.android.internal.database.query.getOrNull 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 @@ -218,15 +218,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val eventIds = ArrayList(eventList.size) val roomMemberContentsByUser = HashMap() - realm.where(CurrentStateEventEntity::class.java) - .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) - .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) - .findAll() - .forEach { - val roomMember = ContentMapper.map(it.root?.content).toModel() - roomMemberContentsByUser[it.stateKey] = roomMember - } - for (event in eventList) { if (event.eventId == null || event.senderId == null) { continue @@ -235,7 +226,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val eventEntity = event.toEntity(roomId, SendState.SYNCED).let { realm.copyToRealmOrUpdate(it) } - if (event.isStateEvent() && event.stateKey != null && !event.isRedacted()) { + if (event.isStateEvent() && event.stateKey != null) { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId root = eventEntity @@ -245,6 +236,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomMemberEventHandler.handle(realm, roomEntity.roomId, event) } } + roomMemberContentsByUser.getOrPut(event.senderId) { + // If we don't have any new state on this user, get it from db + val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root + ContentMapper.map(rootStateEvent?.content).toModel() + } chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) // Give info to crypto module cryptoService.onLiveEvent(roomEntity.roomId, event) From 37230b061455a0a13e3d65f24e533b3600288966 Mon Sep 17 00:00:00 2001 From: Ganard Date: Fri, 31 Jan 2020 16:36:26 +0100 Subject: [PATCH 21/34] Fix issues with read marker and jumpToBottom --- .../internal/database/query/ReadQueries.kt | 24 +++++++++++-------- .../session/room/read/SetReadMarkersTask.kt | 16 ++++++------- .../home/room/detail/RoomDetailFragment.kt | 6 ++--- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt index bf638654d5..1b83577a8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity import io.realm.Realm internal fun isEventRead(monarchy: Monarchy, @@ -37,15 +38,14 @@ internal fun isEventRead(monarchy: Monarchy, monarchy.doWithRealm { realm -> val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm val eventToCheck = liveChunk.timelineEvents.find(eventId) - - isEventRead = if (eventToCheck?.root?.sender == userId) { + isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) { true } else { val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex ?: Int.MIN_VALUE - val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE + val eventToCheckIndex = eventToCheck.displayIndex eventToCheckIndex <= readReceiptIndex } @@ -61,13 +61,17 @@ internal fun isReadMarkerMoreRecent(monarchy: Monarchy, return false } return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false - val eventToCheck = liveChunk.timelineEvents.find(eventId) - + val eventToCheck = TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst() + val eventToCheckChunk = eventToCheck?.chunk?.firstOrNull() val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false - val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.displayIndex - ?: Int.MIN_VALUE - val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE - eventToCheckIndex <= readMarkerIndex + val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = readMarker.eventId).findFirst() + val readMarkerChunk = readMarkerEvent?.chunk?.firstOrNull() + if (eventToCheckChunk == readMarkerChunk) { + val readMarkerIndex = readMarkerEvent?.displayIndex ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE + eventToCheckIndex <= readMarkerIndex + } else { + eventToCheckChunk?.isLastForward == false + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index c6422f5920..4e013f358c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -58,8 +58,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( private val roomFullyReadHandler: RoomFullyReadHandler, private val readReceiptHandler: ReadReceiptHandler, @UserId private val userId: String, - private val eventBus: EventBus, - private val networkConnectivityChecker: NetworkConnectivityChecker + private val eventBus: EventBus ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { @@ -93,13 +92,14 @@ internal class DefaultSetReadMarkersTask @Inject constructor( } val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId - updateDatabase(params.roomId, markers, shouldUpdateRoomSummary) - if (markers.isEmpty()) { - return + if (markers.isNotEmpty() || shouldUpdateRoomSummary) { + updateDatabase(params.roomId, markers, shouldUpdateRoomSummary) } - executeRequest(eventBus) { - isRetryable = true - apiCall = roomAPI.sendReadMarker(params.roomId, markers) + if (markers.isNotEmpty()) { + executeRequest(eventBus) { + isRetryable = true + apiCall = roomAPI.sendReadMarker(params.roomId, markers) + } } } 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 e3f57fc25e..950b865a36 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 @@ -309,11 +309,10 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.observeViewEvents { when (it) { - is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) }.exhaustive } - } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -348,9 +347,8 @@ class RoomDetailFragment @Inject constructor( jumpToBottomView.visibility = View.INVISIBLE if (!roomDetailViewModel.timeline.isLive) { roomDetailViewModel.timeline.restartWithEventId(null) - } else { - layoutManager.scrollToPosition(0) } + layoutManager.scrollToPosition(0) } jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager( From 88755a79b4f19c94d285d7256215343886b010d8 Mon Sep 17 00:00:00 2001 From: Ganard Date: Mon, 3 Feb 2020 15:18:19 +0100 Subject: [PATCH 22/34] In memory sending: fix broken filtering --- .../session/room/timeline/DefaultTimeline.kt | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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 b69b25d09a..0d7e4ccc59 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 @@ -19,11 +19,15 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.RelationType +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.session.room.model.message.MessageContent 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.TimelineSettings +import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited 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 @@ -407,7 +411,7 @@ internal class DefaultTimeline( private fun buildSendingEvents(): List { val sendingEvents = ArrayList() if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { - sendingEvents.addAll(inMemorySendingEvents) + sendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings()) roomEntity?.sendingTimelineEvents ?.where() ?.filterEventsWithSettings() @@ -727,6 +731,23 @@ internal class DefaultTimeline( return this } + private fun List.filterEventsWithSettings(): List { + return filter { + val filterType = if (settings.filterTypes) { + settings.allowedTypes.contains(it.root.type) + } else { + true + } + val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) { + val messageContent = it.root.content.toModel() + messageContent?.relatesTo?.type != RelationType.REPLACE + } else { + true + } + filterType && filterEdits + } + } + private data class State( val hasReachedEnd: Boolean = false, val hasMoreInCache: Boolean = true, From f454078c6b57a64d93adeadac5fa77558fe71b47 Mon Sep 17 00:00:00 2001 From: Ganard Date: Mon, 3 Feb 2020 16:14:36 +0100 Subject: [PATCH 23/34] Clean code --- .idea/codeStyles/Project.xml | 1 + .idea/codeStyles/codeStyleConfig.xml | 1 - .../matrix/android/common/CommonTestHelper.kt | 2 +- .../api/session/room/read/ReadService.kt | 2 +- .../internal/crypto/DefaultCryptoService.kt | 1 - .../internal/crypto/DeviceListManager.kt | 1 - .../database/helper/ChunkEntityHelper.kt | 5 +- .../database/model/CurrentStateEventEntity.kt | 1 - .../internal/database/model/EventEntity.kt | 2 - .../database/model/TimelineEventEntity.kt | 1 - .../query/CurrentStateEventEntityQueries.kt | 5 +- .../query/TimelineEventEntityQueries.kt | 3 +- .../android/internal/di/MatrixComponent.kt | 1 - .../network/NetworkCallbackStrategy.kt | 3 +- .../network/NetworkConnectivityChecker.kt | 6 +- .../android/internal/session/SessionModule.kt | 4 +- .../session/homeserver/CapabilitiesAPI.kt | 2 +- .../session/homeserver/HomeserverPinger.kt | 1 - .../session/room/RoomSummaryUpdater.kt | 2 - .../session/room/draft/DefaultDraftService.kt | 1 - .../session/room/read/DefaultReadService.kt | 1 - .../session/room/read/SetReadMarkersTask.kt | 1 - .../session/room/send/DefaultSendService.kt | 2 - .../room/send/LocalEchoEventFactory.kt | 2 - .../session/room/timeline/DefaultTimeline.kt | 2 - .../room/timeline/TokenChunkEventPersistor.kt | 16 +- .../internal/session/sync/job/SyncService.kt | 1 - tools/check/forbidden_strings_in_code.txt | 3 +- .../core/platform/EllipsizingTextView.java | 445 ------------------ .../core/platform/EllipsizingTextView.kt | 398 ++++++++++++++++ .../riotx/core/platform/VectorBaseFragment.kt | 1 - .../members/RoomMemberListViewModel.kt | 2 +- 32 files changed, 430 insertions(+), 489 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.java create mode 100644 vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index c7053d60c5..40ee4ee5cf 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,6 @@ +

- * Note: {@link android.text.TextUtils.TruncateAt#MARQUEE} ellipsizing type is not supported. - * This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues. - */ -public class EllipsizingTextView extends AppCompatTextView { - public static final int ELLIPSIZE_ALPHA = 0x88; - private SpannableString ELLIPSIS = new SpannableString("\u2026"); - - private static final Pattern DEFAULT_END_PUNCTUATION - = Pattern.compile("[\\.!?,;:\u2026]*$", Pattern.DOTALL); - private final List mEllipsizeListeners = new ArrayList<>(); - private EllipsizeStrategy mEllipsizeStrategy; - private boolean isEllipsized; - private boolean isStale; - private boolean programmaticChange; - private CharSequence mFullText; - private int mMaxLines; - private float mLineSpacingMult = 1.0f; - private float mLineAddVertPad = 0.0f; - - /** - * The end punctuation which will be removed when appending {@link #ELLIPSIS}. - */ - private Pattern mEndPunctPattern; - - public EllipsizingTextView(Context context) { - this(context, null); - } - - - public EllipsizingTextView(Context context, AttributeSet attrs) { - this(context, attrs, android.R.attr.textViewStyle); - } - - - public EllipsizingTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - TypedArray a = context.obtainStyledAttributes(attrs, - new int[]{android.R.attr.maxLines, android.R.attr.ellipsize}, defStyle, 0); - setMaxLines(a.getInt(0, Integer.MAX_VALUE)); - a.recycle(); - setEndPunctuationPattern(DEFAULT_END_PUNCTUATION); - final int currentTextColor = getCurrentTextColor(); - final int ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor)); - ELLIPSIS.setSpan(new ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - public void setEndPunctuationPattern(Pattern pattern) { - mEndPunctPattern = pattern; - } - - @SuppressWarnings("unused") - public void addEllipsizeListener(@NonNull EllipsizeListener listener) { - mEllipsizeListeners.add(listener); - } - - @SuppressWarnings("unused") - public void removeEllipsizeListener(@NonNull EllipsizeListener listener) { - mEllipsizeListeners.remove(listener); - } - - @SuppressWarnings("unused") - public boolean isEllipsized() { - return isEllipsized; - } - - /** - * @return The maximum number of lines displayed in this {@link android.widget.TextView}. - */ - public int getMaxLines() { - return mMaxLines; - } - - @Override - public void setMaxLines(int maxLines) { - super.setMaxLines(maxLines); - mMaxLines = maxLines; - isStale = true; - } - - /** - * Determines if the last fully visible line is being ellipsized. - * - * @return {@code true} if the last fully visible line is being ellipsized; - * otherwise, returns {@code false}. - */ - public boolean ellipsizingLastFullyVisibleLine() { - return mMaxLines == Integer.MAX_VALUE; - } - - @Override - public void setLineSpacing(float add, float mult) { - mLineAddVertPad = add; - mLineSpacingMult = mult; - super.setLineSpacing(add, mult); - } - - @Override - public void setText(CharSequence text, BufferType type) { - if (!programmaticChange) { - mFullText = text instanceof Spanned ? (Spanned) text : text; - isStale = true; - } - super.setText(text, type); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - if (ellipsizingLastFullyVisibleLine()) { - isStale = true; - } - } - - @Override - public void setPadding(int left, int top, int right, int bottom) { - super.setPadding(left, top, right, bottom); - if (ellipsizingLastFullyVisibleLine()) { - isStale = true; - } - } - - @Override - protected void onDraw(@NonNull Canvas canvas) { - if (isStale) { - resetText(); - } - super.onDraw(canvas); - } - - /** - * Sets the ellipsized text if appropriate. - */ - private void resetText() { - int maxLines = getMaxLines(); - CharSequence workingText = mFullText; - boolean ellipsized = false; - - if (maxLines != -1) { - if (mEllipsizeStrategy == null) setEllipsize(null); - workingText = mEllipsizeStrategy.processText(mFullText); - ellipsized = !mEllipsizeStrategy.isInLayout(mFullText); - } - - if (!workingText.equals(getText())) { - programmaticChange = true; - try { - setText(workingText); - } finally { - programmaticChange = false; - } - } - - isStale = false; - if (ellipsized != isEllipsized) { - isEllipsized = ellipsized; - for (EllipsizeListener listener : mEllipsizeListeners) { - listener.ellipsizeStateChanged(ellipsized); - } - } - } - - /** - * Causes words in the text that are longer than the view is wide to be ellipsized - * instead of broken in the middle. Use {@code null} to turn off ellipsizing. - *

- * Note: Method does nothing for {@link android.text.TextUtils.TruncateAt#MARQUEE} - * ellipsizing type. - * - * @param where part of text to ellipsize - */ - @Override - public void setEllipsize(TruncateAt where) { - if (where == null) { - mEllipsizeStrategy = new EllipsizeNoneStrategy(); - return; - } - - switch (where) { - case END: - mEllipsizeStrategy = new EllipsizeEndStrategy(); - break; - case START: - mEllipsizeStrategy = new EllipsizeStartStrategy(); - break; - case MIDDLE: - mEllipsizeStrategy = new EllipsizeMiddleStrategy(); - break; - case MARQUEE: - default: - mEllipsizeStrategy = new EllipsizeNoneStrategy(); - break; - } - } - - /** - * A listener that notifies when the ellipsize state has changed. - */ - public interface EllipsizeListener { - void ellipsizeStateChanged(boolean ellipsized); - } - - /** - * A base class for an ellipsize strategy. - */ - private abstract class EllipsizeStrategy { - /** - * Returns ellipsized text if the text does not fit inside of the layout; - * otherwise, returns the full text. - * - * @param text text to process - * @return Ellipsized text if the text does not fit inside of the layout; - * otherwise, returns the full text. - */ - public CharSequence processText(CharSequence text) { - return !isInLayout(text) ? createEllipsizedText(text) : text; - } - - /** - * Determines if the text fits inside of the layout. - * - * @param text text to fit - * @return {@code true} if the text fits inside of the layout; - * otherwise, returns {@code false}. - */ - public boolean isInLayout(CharSequence text) { - Layout layout = createWorkingLayout(text); - return layout.getLineCount() <= getLinesCount(); - } - - /** - * Creates a working layout with the given text. - * - * @param workingText text to create layout with - * @return {@link android.text.Layout} with the given text. - */ - protected Layout createWorkingLayout(CharSequence workingText) { - return new StaticLayout(workingText, getPaint(), - getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(), - Alignment.ALIGN_NORMAL, mLineSpacingMult, - mLineAddVertPad, false /* includepad */); - } - - /** - * Get how many lines of text we are allowed to display. - */ - protected int getLinesCount() { - if (ellipsizingLastFullyVisibleLine()) { - int fullyVisibleLinesCount = getFullyVisibleLinesCount(); - return fullyVisibleLinesCount == -1 ? 1 : fullyVisibleLinesCount; - } else { - return mMaxLines; - } - } - - /** - * Get how many lines of text we can display so their full height is visible. - */ - protected int getFullyVisibleLinesCount() { - Layout layout = createWorkingLayout(""); - int height = getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); - int lineHeight = layout.getLineBottom(0); - return height / lineHeight; - } - - /** - * Creates ellipsized text from the given text. - * - * @param fullText text to ellipsize - * @return Ellipsized text - */ - protected abstract CharSequence createEllipsizedText(CharSequence fullText); - } - - /** - * An {@link EllipsizingTextView.EllipsizeStrategy} that - * does not ellipsize text. - */ - private class EllipsizeNoneStrategy extends EllipsizeStrategy { - @Override - protected CharSequence createEllipsizedText(CharSequence fullText) { - return fullText; - } - } - - /** - * An {@link EllipsizingTextView.EllipsizeStrategy} that - * ellipsizes text at the end. - */ - private class EllipsizeEndStrategy extends EllipsizeStrategy { - @Override - protected CharSequence createEllipsizedText(CharSequence fullText) { - Layout layout = createWorkingLayout(fullText); - int cutOffIndex = layout.getLineEnd(mMaxLines - 1); - int textLength = fullText.length(); - int cutOffLength = textLength - cutOffIndex; - if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length(); - CharSequence workingText = TextUtils.substring(fullText, 0, textLength - cutOffLength).trim(); - - while (!isInLayout(TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS))) { - int lastSpace = TextUtils.lastIndexOf(workingText, ' '); - if (lastSpace == -1) { - break; - } - workingText = TextUtils.substring(workingText, 0, lastSpace).trim(); - } - - workingText = TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS); - SpannableStringBuilder dest = new SpannableStringBuilder(workingText); - - if (fullText instanceof Spanned) { - TextUtils.copySpansFrom((Spanned) fullText, 0, workingText.length(), null, dest, 0); - } - return dest; - } - - /** - * Strips the end punctuation from a given text according to {@link #mEndPunctPattern}. - * - * @param workingText text to strip end punctuation from - * @return Text without end punctuation. - */ - public String stripEndPunctuation(CharSequence workingText) { - return mEndPunctPattern.matcher(workingText).replaceFirst(""); - } - } - - /** - * An {@link EllipsizingTextView.EllipsizeStrategy} that - * ellipsizes text at the start. - */ - private class EllipsizeStartStrategy extends EllipsizeStrategy { - @Override - protected CharSequence createEllipsizedText(CharSequence fullText) { - Layout layout = createWorkingLayout(fullText); - int cutOffIndex = layout.getLineEnd(mMaxLines - 1); - int textLength = fullText.length(); - int cutOffLength = textLength - cutOffIndex; - if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length(); - CharSequence workingText = TextUtils.substring(fullText, cutOffLength, textLength).trim(); - - while (!isInLayout(TextUtils.concat(ELLIPSIS, workingText))) { - int firstSpace = TextUtils.indexOf(workingText, ' '); - if (firstSpace == -1) { - break; - } - workingText = TextUtils.substring(workingText, firstSpace, workingText.length()).trim(); - } - - workingText = TextUtils.concat(ELLIPSIS, workingText); - SpannableStringBuilder dest = new SpannableStringBuilder(workingText); - - if (fullText instanceof Spanned) { - TextUtils.copySpansFrom((Spanned) fullText, textLength - workingText.length(), - textLength, null, dest, 0); - } - return dest; - } - } - - /** - * An {@link EllipsizingTextView.EllipsizeStrategy} that - * ellipsizes text in the middle. - */ - private class EllipsizeMiddleStrategy extends EllipsizeStrategy { - @Override - protected CharSequence createEllipsizedText(CharSequence fullText) { - Layout layout = createWorkingLayout(fullText); - int cutOffIndex = layout.getLineEnd(mMaxLines - 1); - int textLength = fullText.length(); - int cutOffLength = textLength - cutOffIndex; - if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length(); - cutOffLength += cutOffIndex % 2; // Make it even. - String firstPart = TextUtils.substring( - fullText, 0, textLength / 2 - cutOffLength / 2).trim(); - String secondPart = TextUtils.substring( - fullText, textLength / 2 + cutOffLength / 2, textLength).trim(); - - while (!isInLayout(TextUtils.concat(firstPart, ELLIPSIS, secondPart))) { - int lastSpaceFirstPart = firstPart.lastIndexOf(' '); - int firstSpaceSecondPart = secondPart.indexOf(' '); - if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break; - firstPart = firstPart.substring(0, lastSpaceFirstPart).trim(); - secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length()).trim(); - } - - SpannableStringBuilder firstDest = new SpannableStringBuilder(firstPart); - SpannableStringBuilder secondDest = new SpannableStringBuilder(secondPart); - - if (fullText instanceof Spanned) { - TextUtils.copySpansFrom((Spanned) fullText, 0, firstPart.length(), - null, firstDest, 0); - TextUtils.copySpansFrom((Spanned) fullText, textLength - secondPart.length(), - textLength, null, secondDest, 0); - } - return TextUtils.concat(firstDest, ELLIPSIS, secondDest); - } - } -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt new file mode 100644 index 0000000000..cfb8bff66a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt @@ -0,0 +1,398 @@ +/* + * 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.platform + +import android.R +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.text.Layout +import android.text.Spannable +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.StaticLayout +import android.text.TextUtils.* +import android.text.style.ForegroundColorSpan +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import java.util.ArrayList +import java.util.regex.Pattern + +/** + * A [android.widget.TextView] that ellipsizes more intelligently. + * This class supports ellipsizing multiline text through setting `android:ellipsize` + * and `android:maxLines`. + * + * + * Note: [TruncateAt.MARQUEE] ellipsizing type is not supported. + * This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues. + */ +class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = R.attr.textViewStyle) + : AppCompatTextView(context, attrs, defStyle) { + + private val ELLIPSIS = SpannableString("\u2026") + private val ellipsizeListeners: MutableList = ArrayList() + private var ellipsizeStrategy: EllipsizeStrategy? = null + var isEllipsized = false + private set + private var isStale = false + private var programmaticChange = false + private var fullText: CharSequence? = null + private var maxLines = 0 + private var lineSpacingMult = 1.0f + private var lineAddVertPad = 0.0f + /** + * The end punctuation which will be removed when appending [.ELLIPSIS]. + */ + private var mEndPunctPattern: Pattern? = null + + fun setEndPunctuationPattern(pattern: Pattern?) { + mEndPunctPattern = pattern + } + + fun addEllipsizeListener(listener: EllipsizeListener) { + ellipsizeListeners.add(listener) + } + + fun removeEllipsizeListener(listener: EllipsizeListener) { + ellipsizeListeners.remove(listener) + } + + /** + * @return The maximum number of lines displayed in this [android.widget.TextView]. + */ + override fun getMaxLines(): Int { + return maxLines + } + + override fun setMaxLines(maxLines: Int) { + super.setMaxLines(maxLines) + this.maxLines = maxLines + isStale = true + } + + /** + * Determines if the last fully visible line is being ellipsized. + * + * @return `true` if the last fully visible line is being ellipsized; + * otherwise, returns `false`. + */ + fun ellipsizingLastFullyVisibleLine(): Boolean { + return maxLines == Int.MAX_VALUE + } + + override fun setLineSpacing(add: Float, mult: Float) { + lineAddVertPad = add + lineSpacingMult = mult + super.setLineSpacing(add, mult) + } + + override fun setText(text: CharSequence, type: BufferType) { + if (!programmaticChange) { + fullText = if (text is Spanned) text else text + isStale = true + } + super.setText(text, type) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + if (ellipsizingLastFullyVisibleLine()) { + isStale = true + } + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + super.setPadding(left, top, right, bottom) + if (ellipsizingLastFullyVisibleLine()) { + isStale = true + } + } + + override fun onDraw(canvas: Canvas) { + if (isStale) { + resetText() + } + super.onDraw(canvas) + } + + /** + * Sets the ellipsized text if appropriate. + */ + private fun resetText() { + val maxLines = maxLines + var workingText = fullText + var ellipsized = false + if (maxLines != -1) { + if (ellipsizeStrategy == null) setEllipsize(null) + workingText = ellipsizeStrategy!!.processText(fullText) + ellipsized = !ellipsizeStrategy!!.isInLayout(fullText) + } + if (workingText != text) { + programmaticChange = true + text = try { + workingText + } finally { + programmaticChange = false + } + } + isStale = false + if (ellipsized != isEllipsized) { + isEllipsized = ellipsized + for (listener in ellipsizeListeners) { + listener.ellipsizeStateChanged(ellipsized) + } + } + } + + /** + * Causes words in the text that are longer than the view is wide to be ellipsized + * instead of broken in the middle. Use `null` to turn off ellipsizing. + * + * + * Note: Method does nothing for [TruncateAt.MARQUEE] + * ellipsizing type. + * + * @param where part of text to ellipsize + */ + override fun setEllipsize(where: TruncateAt?) { + if (where == null) { + ellipsizeStrategy = EllipsizeNoneStrategy() + return + } + ellipsizeStrategy = when (where) { + TruncateAt.END -> EllipsizeEndStrategy() + TruncateAt.START -> EllipsizeStartStrategy() + TruncateAt.MIDDLE -> EllipsizeMiddleStrategy() + TruncateAt.MARQUEE -> EllipsizeNoneStrategy() + else -> EllipsizeNoneStrategy() + } + } + + /** + * A listener that notifies when the ellipsize state has changed. + */ + interface EllipsizeListener { + fun ellipsizeStateChanged(ellipsized: Boolean) + } + + /** + * A base class for an ellipsize strategy. + */ + private abstract inner class EllipsizeStrategy { + /** + * Returns ellipsized text if the text does not fit inside of the layout; + * otherwise, returns the full text. + * + * @param text text to process + * @return Ellipsized text if the text does not fit inside of the layout; + * otherwise, returns the full text. + */ + fun processText(text: CharSequence?): CharSequence? { + return if (!isInLayout(text)) createEllipsizedText(text) else text + } + + /** + * Determines if the text fits inside of the layout. + * + * @param text text to fit + * @return `true` if the text fits inside of the layout; + * otherwise, returns `false`. + */ + fun isInLayout(text: CharSequence?): Boolean { + val layout = createWorkingLayout(text) + return layout.lineCount <= linesCount + } + + /** + * Creates a working layout with the given text. + * + * @param workingText text to create layout with + * @return [android.text.Layout] with the given text. + */ + @Suppress("DEPRECATION") + protected fun createWorkingLayout(workingText: CharSequence?): Layout { + return StaticLayout( + workingText, + paint, + width - compoundPaddingLeft - compoundPaddingRight, + Layout.Alignment.ALIGN_NORMAL, + lineSpacingMult, + lineAddVertPad, + false + ) + } + + /** + * Get how many lines of text we are allowed to display. + */ + protected val linesCount: Int + get() = if (ellipsizingLastFullyVisibleLine()) { + val fullyVisibleLinesCount = fullyVisibleLinesCount + if (fullyVisibleLinesCount == -1) 1 else fullyVisibleLinesCount + } else { + maxLines + } + + /** + * Get how many lines of text we can display so their full height is visible. + */ + protected val fullyVisibleLinesCount: Int + get() { + val layout = createWorkingLayout("") + val height = height - compoundPaddingTop - compoundPaddingBottom + val lineHeight = layout.getLineBottom(0) + return height / lineHeight + } + + /** + * Creates ellipsized text from the given text. + * + * @param fullText text to ellipsize + * @return Ellipsized text + */ + protected abstract fun createEllipsizedText(fullText: CharSequence?): CharSequence? + } + + /** + * An [EllipsizingTextView.EllipsizeStrategy] that + * does not ellipsize text. + */ + private inner class EllipsizeNoneStrategy : EllipsizeStrategy() { + override fun createEllipsizedText(fullText: CharSequence?): CharSequence? { + return fullText + } + } + + /** + * An [EllipsizingTextView.EllipsizeStrategy] that + * ellipsizes text at the end. + */ + private inner class EllipsizeEndStrategy : EllipsizeStrategy() { + override fun createEllipsizedText(fullText: CharSequence?): CharSequence? { + val layout = createWorkingLayout(fullText) + val cutOffIndex = layout.getLineEnd(maxLines - 1) + val textLength = fullText!!.length + var cutOffLength = textLength - cutOffIndex + if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length + var workingText: CharSequence = substring(fullText, 0, textLength - cutOffLength).trim() + while (!isInLayout(concat(stripEndPunctuation(workingText), ELLIPSIS))) { + val lastSpace = lastIndexOf(workingText, ' ') + if (lastSpace == -1) { + break + } + workingText = substring(workingText, 0, lastSpace).trim() + } + workingText = concat(stripEndPunctuation(workingText), ELLIPSIS) + val dest = SpannableStringBuilder(workingText) + if (fullText is Spanned) { + copySpansFrom(fullText as Spanned?, 0, workingText.length, null, dest, 0) + } + return dest + } + + /** + * Strips the end punctuation from a given text according to [.mEndPunctPattern]. + * + * @param workingText text to strip end punctuation from + * @return Text without end punctuation. + */ + fun stripEndPunctuation(workingText: CharSequence?): String { + return mEndPunctPattern!!.matcher(workingText).replaceFirst("") + } + } + + /** + * An [EllipsizingTextView.EllipsizeStrategy] that + * ellipsizes text at the start. + */ + private inner class EllipsizeStartStrategy : EllipsizeStrategy() { + override fun createEllipsizedText(fullText: CharSequence?): CharSequence? { + val layout = createWorkingLayout(fullText) + val cutOffIndex = layout.getLineEnd(maxLines - 1) + val textLength = fullText!!.length + var cutOffLength = textLength - cutOffIndex + if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length + var workingText: CharSequence = substring(fullText, cutOffLength, textLength).trim() + while (!isInLayout(concat(ELLIPSIS, workingText))) { + val firstSpace = indexOf(workingText, ' ') + if (firstSpace == -1) { + break + } + workingText = substring(workingText, firstSpace, workingText.length).trim() + } + workingText = concat(ELLIPSIS, workingText) + val dest = SpannableStringBuilder(workingText) + if (fullText is Spanned) { + copySpansFrom(fullText as Spanned?, textLength - workingText.length, + textLength, null, dest, 0) + } + return dest + } + } + + /** + * An [EllipsizingTextView.EllipsizeStrategy] that + * ellipsizes text in the middle. + */ + private inner class EllipsizeMiddleStrategy : EllipsizeStrategy() { + override fun createEllipsizedText(fullText: CharSequence?): CharSequence? { + val layout = createWorkingLayout(fullText) + val cutOffIndex = layout.getLineEnd(maxLines - 1) + val textLength = fullText!!.length + var cutOffLength = textLength - cutOffIndex + if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length + cutOffLength += cutOffIndex % 2 // Make it even. + var firstPart = substring( + fullText, 0, textLength / 2 - cutOffLength / 2).trim() + var secondPart = substring( + fullText, textLength / 2 + cutOffLength / 2, textLength).trim() + while (!isInLayout(concat(firstPart, ELLIPSIS, secondPart))) { + val lastSpaceFirstPart = firstPart.lastIndexOf(' ') + val firstSpaceSecondPart = secondPart.indexOf(' ') + if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break + firstPart = firstPart.substring(0, lastSpaceFirstPart).trim() + secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length).trim() + } + val firstDest = SpannableStringBuilder(firstPart) + val secondDest = SpannableStringBuilder(secondPart) + if (fullText is Spanned) { + copySpansFrom(fullText as Spanned?, 0, firstPart.length, + null, firstDest, 0) + copySpansFrom(fullText as Spanned?, textLength - secondPart.length, + textLength, null, secondDest, 0) + } + return concat(firstDest, ELLIPSIS, secondDest) + } + } + + companion object { + const val ELLIPSIZE_ALPHA = 0x88 + private val DEFAULT_END_PUNCTUATION = Pattern.compile("[.!?,;:\u2026]*$", Pattern.DOTALL) + } + + init { + val a = context.obtainStyledAttributes(attrs, intArrayOf(R.attr.maxLines, R.attr.ellipsize), defStyle, 0) + maxLines = a.getInt(0, Int.MAX_VALUE) + a.recycle() + setEndPunctuationPattern(DEFAULT_END_PUNCTUATION) + val currentTextColor = currentTextColor + val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor)) + ELLIPSIS.setSpan(ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } +} \ No newline at end of file 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 6ce3b7449d..8261b5991b 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 @@ -33,7 +33,6 @@ import androidx.annotation.MainThread import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders import butterknife.ButterKnife import butterknife.Unbinder import com.airbnb.mvrx.BaseMvRxFragment diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt index 87c3a02d91..9bf8f88518 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt @@ -74,7 +74,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState .combineLatest, PowerLevelsContent, RoomMemberSummaries>( room.rx(session).liveRoomMembers(roomMemberQueryParams), room.rx(session) - .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS,"") + .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "") .mapOptional { it.content.toModel() } .unwrap(), BiFunction { roomMembers, powerLevelsContent -> From 1728d31401ab6c7c9daf15aa8c86d634fdad4bc8 Mon Sep 17 00:00:00 2001 From: Ganard Date: Mon, 3 Feb 2020 18:45:50 +0100 Subject: [PATCH 24/34] Fix some issues and make test passes --- .../matrix/android/common/CryptoTestHelper.kt | 5 + .../session/room/timeline/ChunkEntityTest.kt | 101 ++++++------------ .../database/helper/ChunkEntityHelper.kt | 88 ++++++++------- .../network/NetworkConnectivityChecker.kt | 1 - .../session/room/timeline/DefaultTimeline.kt | 18 ++-- .../room/timeline/TokenChunkEventPersistor.kt | 1 - .../core/platform/EllipsizingTextView.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 4 +- .../room/detail/ScrollOnNewMessageCallback.kt | 10 ++ 9 files changed, 112 insertions(+), 118 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt index 59db3b287c..1bc7514b3e 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt @@ -241,6 +241,11 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { val bobEventsListener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List) { + // noop } override fun onTimelineUpdated(snapshot: List) { 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 f720672e0b..7943a240e5 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 @@ -20,15 +20,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest 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.api.session.room.send.SendState +import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.helper.merge +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.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 -import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject @@ -58,8 +58,11 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldAdd_whenNotAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent() - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) + + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let { + realm.copyToRealmOrUpdate(it) + } + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) chunk.timelineEvents.size shouldEqual 1 } } @@ -68,65 +71,23 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldNotAdd_whenAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent() - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let { + realm.copyToRealmOrUpdate(it) + } + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) chunk.timelineEvents.size shouldEqual 1 } } - @Test - fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() { - monarchy.runTransactionSync { realm -> - val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeRoomMemberEvent() - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) - chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 - } - } - - @Test - fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() { - monarchy.runTransactionSync { realm -> - val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent() - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) - chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 - } - } - - @Test - fun addAll_shouldStateIndexIncremented_whenStateEventsAreAddedForward() { - monarchy.runTransactionSync { realm -> - val chunk: ChunkEntity = realm.createObject() - val fakeEvents = createFakeListOfEvents(30) - val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size - chunk.addAll("roomId", fakeEvents, PaginationDirection.FORWARDS) - chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents - } - } - - @Test - fun addAll_shouldStateIndexDecremented_whenStateEventsAreAddedBackward() { - monarchy.runTransactionSync { realm -> - val chunk: ChunkEntity = realm.createObject() - val fakeEvents = createFakeListOfEvents(30) - val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size - val lastIsState = fakeEvents.last().isStateEvent() - val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents - chunk.addAll("roomId", fakeEvents, PaginationDirection.BACKWARDS) - chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex - } - } - @Test fun merge_shouldAddEvents_whenMergingBackward() { monarchy.runTransactionSync { realm -> val chunk1: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) chunk1.timelineEvents.size shouldEqual 60 } } @@ -140,9 +101,9 @@ internal class ChunkEntityTest : InstrumentedTest { val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10) chunk1.isLastForward = true chunk2.isLastForward = false - chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS) - chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS) + chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) chunk1.timelineEvents.size shouldEqual 40 chunk1.isLastForward.shouldBeTrue() } @@ -155,9 +116,9 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val prevToken = "prev_token" chunk1.prevToken = prevToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS) + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS) chunk1.prevToken shouldEqual prevToken } } @@ -169,19 +130,25 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val nextToken = "next_token" chunk1.nextToken = nextToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) chunk1.nextToken shouldEqual nextToken } } private fun ChunkEntity.addAll(roomId: String, events: List, - direction: PaginationDirection, - stateIndexOffset: Int = 0) { + direction: PaginationDirection) { events.forEach { event -> - add(roomId, event, direction) + val fakeEvent = event.toEntity(roomId, SendState.SYNCED).let { + realm.copyToRealmOrUpdate(it) + } + addTimelineEvent(roomId, fakeEvent, direction, emptyMap()) } } + + companion object { + private const val ROOM_ID = "roomId" + } } 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 0f9c2ae124..80376fb6ee 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 @@ -62,27 +62,7 @@ internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direct } return eventsToMerge .forEach { - if (timelineEvents.find(it.eventId) == null) { - val eventId = it.eventId - if (timelineEvents.find(eventId) != null) { - return - } - val displayIndex = nextDisplayIndex(direction) - val localId = TimelineEventEntity.nextId(realm) - val copied = localRealm.createObject().apply { - this.localId = localId - this.root = it.root - this.eventId = it.eventId - this.roomId = it.roomId - this.annotations = it.annotations - this.readReceipts = it.readReceipts - this.displayIndex = displayIndex - this.senderAvatar = it.senderAvatar - this.senderName = it.senderName - this.isUniqueDisplayName = it.isUniqueDisplayName - } - timelineEvents.add(copied) - } + addTimelineEventFromMerge(localRealm, it, direction) } } @@ -108,7 +88,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, internal fun ChunkEntity.addTimelineEvent(roomId: String, eventEntity: EventEntity, direction: PaginationDirection, - roomMemberContentsByUser: HashMap) { + roomMemberContentsByUser: Map) { val eventId = eventEntity.eventId if (timelineEvents.find(eventId) != null) { return @@ -130,28 +110,60 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, val roomMemberContent = roomMemberContentsByUser[senderId] this.senderAvatar = roomMemberContent?.avatarUrl this.senderName = roomMemberContent?.displayName - if (roomMemberContent?.displayName != null) { - val isHistoricalUnique = roomMemberContentsByUser.values.find { - it != roomMemberContent && it?.displayName == roomMemberContent.displayName - } == null - isUniqueDisplayName = if (isLastForward) { - val isLiveUnique = RoomMemberSummaryEntity - .where(realm, roomId) - .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, roomMemberContent.displayName) - .findAll().none { - !roomMemberContentsByUser.containsKey(it.userId) - } - isHistoricalUnique && isLiveUnique - } else { - isHistoricalUnique - } + isUniqueDisplayName = if (roomMemberContent?.displayName != null) { + computeIsUnique(realm, roomId, isLastForward, roomMemberContent, roomMemberContentsByUser) } else { - isUniqueDisplayName = true + true } } timelineEvents.add(timelineEventEntity) } +private fun computeIsUnique( + realm: Realm, + roomId: String, + isLastForward: Boolean, + myRoomMemberContent: RoomMemberContent, + roomMemberContentsByUser: Map +): Boolean { + val isHistoricalUnique = roomMemberContentsByUser.values.find { + it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName + } == null + return if (isLastForward) { + val isLiveUnique = RoomMemberSummaryEntity + .where(realm, roomId) + .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName) + .findAll().none { + !roomMemberContentsByUser.containsKey(it.userId) + } + isHistoricalUnique && isLiveUnique + } else { + isHistoricalUnique + } +} + +private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEntity: TimelineEventEntity, direction: PaginationDirection) { + val eventId = timelineEventEntity.eventId + if (timelineEvents.find(eventId) != null) { + return + } + val displayIndex = nextDisplayIndex(direction) + val localId = TimelineEventEntity.nextId(realm) + val copied = realm.createObject().apply { + this.localId = localId + this.root = timelineEventEntity.root + this.eventId = timelineEventEntity.eventId + this.roomId = timelineEventEntity.roomId + this.annotations = timelineEventEntity.annotations + this.readReceipts = timelineEventEntity.readReceipts + this.displayIndex = displayIndex + this.senderAvatar = timelineEventEntity.senderAvatar + this.senderName = timelineEventEntity.senderName + this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName + } + timelineEvents.add(copied) +} + private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst() ?: realm.createObject(eventEntity.eventId).apply { 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 8bb01ba644..881a6a45b1 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 @@ -20,7 +20,6 @@ import androidx.annotation.WorkerThread import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.homeserver.HomeServerPinger import im.vector.matrix.android.internal.util.BackgroundDetectionObserver -import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.runBlocking import java.util.Collections import java.util.concurrent.atomic.AtomicBoolean 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 1357170aaf..d3b0787b68 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 @@ -120,7 +120,7 @@ internal class DefaultTimeline( if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener } - handleUpdates(changeSet) + handleUpdates(results, changeSet) } private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> @@ -185,7 +185,7 @@ internal class DefaultTimeline( .filterEventsWithSettings() .findAll() handleInitialLoad() - filteredEvents.addChangeListener(eventsChangeListener) + nonFilteredEvents.addChangeListener(eventsChangeListener) eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) .findAllAsync() @@ -215,8 +215,8 @@ internal class DefaultTimeline( if (this::eventRelations.isInitialized) { eventRelations.removeAllChangeListeners() } - if (this::filteredEvents.isInitialized) { - filteredEvents.removeAllChangeListeners() + if (this::nonFilteredEvents.isInitialized) { + nonFilteredEvents.removeAllChangeListeners() } if (settings.shouldHandleHiddenReadReceipts()) { hiddenReadReceipts.dispose() @@ -452,7 +452,7 @@ internal class DefaultTimeline( var shouldFetchInitialEvent = false val currentInitialEventId = initialEventId val initialDisplayIndex = if (currentInitialEventId == null) { - filteredEvents.firstOrNull()?.displayIndex + nonFilteredEvents.firstOrNull()?.displayIndex } else { val initialEvent = nonFilteredEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) @@ -480,7 +480,7 @@ internal class DefaultTimeline( /** * This has to be called on TimelineThread as it access realm live results */ - private fun handleUpdates(changeSet: OrderedCollectionChangeSet) { + private fun handleUpdates(results: RealmResults, changeSet: OrderedCollectionChangeSet) { // If changeSet has deletion we are having a gap, so we clear everything if (changeSet.deletionRanges.isNotEmpty()) { clearAllValues() @@ -488,9 +488,9 @@ internal class DefaultTimeline( var postSnapshot = false changeSet.insertionRanges.forEach { range -> val (startDisplayIndex, direction) = if (range.startIndex == 0) { - Pair(filteredEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) + Pair(results[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) } else { - Pair(filteredEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) + Pair(results[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) } val state = getState(direction) if (state.isPaginating) { @@ -503,7 +503,7 @@ internal class DefaultTimeline( } } changeSet.changes.forEach { index -> - val eventEntity = filteredEvents[index] + val eventEntity = results[index] eventEntity?.eventId?.let { eventId -> postSnapshot = rebuildEvent(eventId) { buildTimelineEvent(eventEntity) 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 6209fbcfdb..41920158c4 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 @@ -167,7 +167,6 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { Timber.v("Reach end of $roomId") - roomId.isBlank() if (direction == PaginationDirection.FORWARDS) { val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) if (currentChunk != currentLiveChunk) { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt index cfb8bff66a..7313fd4f79 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt @@ -395,4 +395,4 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor)) ELLIPSIS.setSpan(ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } -} \ No newline at end of file +} 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 e9e20eba8f..e09270b52b 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 @@ -350,9 +350,11 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) jumpToBottomView.visibility = View.INVISIBLE if (!roomDetailViewModel.timeline.isLive) { + scrollOnNewMessageCallback.forceScrollOnNextUpdate() roomDetailViewModel.timeline.restartWithEventId(null) + } else { + layoutManager.scrollToPosition(0) } - layoutManager.scrollToPosition(0) } jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt index 6a1646f009..a18ee31e2c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -27,12 +27,22 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { private val newTimelineEventIds = CopyOnWriteArrayList() + private var forceScroll = false fun addNewTimelineEventIds(eventIds: List) { newTimelineEventIds.addAll(0, eventIds) } + fun forceScrollOnNextUpdate() { + forceScroll = true + } + override fun onInserted(position: Int, count: Int) { + if (forceScroll) { + forceScroll = false + layoutManager.scrollToPosition(position) + return + } Timber.v("On inserted $count count at position: $position") if (layoutManager.findFirstVisibleItemPosition() != position) { return From ed9c3379bffb7effa9f0c569299e8029dfb2c78c Mon Sep 17 00:00:00 2001 From: Ganard Date: Tue, 4 Feb 2020 13:50:10 +0100 Subject: [PATCH 25/34] Rename file --- .../homeserver/{HomeserverPinger.kt => HomeServerPinger.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/{HomeserverPinger.kt => HomeServerPinger.kt} (100%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeserverPinger.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt similarity index 100% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeserverPinger.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt From a17ec14dd7c8430200dbb201d20cfff609ce3c03 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Feb 2020 11:39:23 +0100 Subject: [PATCH 26/34] Cleanup and little change on Throwable logging --- .idea/codeStyles/codeStyleConfig.xml | 1 + .../matrix/android/api/pushrules/RoomMemberCountCondition.kt | 2 +- .../crypto/crosssigning/DefaultCrossSigningService.kt | 2 +- .../matrix/android/internal/crypto/keysbackup/KeysBackup.kt | 2 +- .../vector/matrix/android/internal/session/SessionModule.kt | 5 +++++ .../android/internal/session/room/RoomSummaryUpdater.kt | 2 +- .../internal/session/room/timeline/TimelineEventDecryptor.kt | 2 +- .../matrix/android/internal/session/sync/job/SyncThread.kt | 2 +- .../im/vector/matrix/android/internal/task/TaskExecutor.kt | 2 +- 9 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 79ee123c2b..6e6eec1148 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RoomMemberCountCondition.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RoomMemberCountCondition.kt index d41788234b..34bdbffefe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RoomMemberCountCondition.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RoomMemberCountCondition.kt @@ -59,7 +59,7 @@ class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_coun val (prefix, count) = match.destructured return prefix to count.toInt() } catch (t: Throwable) { - Timber.d(t) + Timber.e(t, "Unable to parse 'is' field") } return null } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 7dec3236a3..4632a05552 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -393,7 +393,7 @@ internal class DefaultCrossSigningService @Inject constructor( return@forEach } catch (failure: Throwable) { // log - Timber.v(failure) + Timber.w(failure, "Signature not valid?") } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt index 73646de361..7906005046 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt @@ -414,7 +414,7 @@ internal class KeysBackup @Inject constructor( olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature) isSignatureValid = true } catch (e: OlmException) { - Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}") + Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}") } } 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 571f271933..b1916bc826 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 @@ -66,6 +66,11 @@ internal abstract class SessionModule { companion object { internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5" + /** + * Rules: + * Annotate methods with @SessionScope only the @Provides annotated methods with computation and logic. + */ + @JvmStatic @Provides fun providesHomeServerConnectionConfig(sessionParams: SessionParams): HomeServerConnectionConfig { 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 e43962a3ff..6a26e6d282 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 @@ -51,8 +51,8 @@ internal class RoomSummaryUpdater @Inject constructor( private val roomAvatarResolver: RoomAvatarResolver, private val monarchy: Monarchy) { - // TODO: maybe allow user of SDK to give that list companion object { + // TODO: maybe allow user of SDK to give that list val PREVIEWABLE_TYPES = listOf( // TODO filter message type (KEY_VERIFICATION_READY, etc.) EventType.MESSAGE, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt index 967dd66397..800e6c8d11 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -110,7 +110,7 @@ internal class TimelineEventDecryptor( eventEntity.setDecryptionResult(result) } } catch (e: MXCryptoError) { - Timber.v(e, "Failed to decrypt event $eventId") + Timber.w(e, "Failed to decrypt event $eventId") if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { // Keep track of unknown sessions to automatically try to decrypt on new session realm.executeTransaction { 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 db197b63df..aefff5bd75 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 @@ -174,7 +174,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, Timber.v("Cancelled") } else if (failure.isTokenError()) { // No token or invalid token, stop the thread - Timber.w(failure) + Timber.w(failure, "Token error") isStarted = false isTokenValid = false } else { 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 ec78173190..fc599d2939 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 @@ -47,7 +47,7 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers } resultOrFailure .onFailure { - Timber.d(it, "Task failed") + Timber.e(it, "Task failed") } .foldToCallback(task.callback) } From b8096f21ea6eb60869b5a8a3ad318d272f607f3f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Feb 2020 11:56:07 +0100 Subject: [PATCH 27/34] Restore Copyright and cleanup --- .../core/platform/EllipsizingTextView.kt | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt index 7313fd4f79..a0956d0528 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt @@ -1,4 +1,8 @@ /* + * Copyright (C) 2011 Micah Hainline + * Copyright (C) 2012 Triposo + * Copyright (C) 2013 Paul Imhoff + * Copyright (C) 2014 Shahin Yousefi * Copyright 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,7 +20,6 @@ */ package im.vector.riotx.core.platform -import android.R import android.content.Context import android.graphics.Canvas import android.graphics.Color @@ -26,13 +29,22 @@ import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned import android.text.StaticLayout -import android.text.TextUtils.* +import android.text.TextUtils.TruncateAt +import android.text.TextUtils.concat +import android.text.TextUtils.copySpansFrom +import android.text.TextUtils.indexOf +import android.text.TextUtils.lastIndexOf +import android.text.TextUtils.substring import android.text.style.ForegroundColorSpan import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView import java.util.ArrayList import java.util.regex.Pattern +/* + * Imported from https://gist.github.com/hateum/d2095575b441007d62b8 + */ + /** * A [android.widget.TextView] that ellipsizes more intelligently. * This class supports ellipsizing multiline text through setting `android:ellipsize` @@ -42,7 +54,7 @@ import java.util.regex.Pattern * Note: [TruncateAt.MARQUEE] ellipsizing type is not supported. * This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues. */ -class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = R.attr.textViewStyle) +class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = android.R.attr.textViewStyle) : AppCompatTextView(context, attrs, defStyle) { private val ELLIPSIS = SpannableString("\u2026") @@ -387,7 +399,7 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att } init { - val a = context.obtainStyledAttributes(attrs, intArrayOf(R.attr.maxLines, R.attr.ellipsize), defStyle, 0) + val a = context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle, 0) maxLines = a.getInt(0, Int.MAX_VALUE) a.recycle() setEndPunctuationPattern(DEFAULT_END_PUNCTUATION) From 47ee2a24a74d6ef2b990f5848772aed94ca67c8e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Feb 2020 11:58:08 +0100 Subject: [PATCH 28/34] Move NoOpMatrixCallback to MatrixCallback.kt file --- .../matrix/android/api/MatrixCallback.kt | 5 +++++ .../riotx/core/utils/NoOpMatrixCallback.kt | 21 ------------------- .../home/room/detail/RoomDetailViewModel.kt | 2 +- 3 files changed, 6 insertions(+), 22 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixCallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixCallback.kt index c2f70ce5dc..e57117f644 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixCallback.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixCallback.kt @@ -38,3 +38,8 @@ interface MatrixCallback { // no-op } } + +/** + * Basic no op implementation + */ +class NoOpMatrixCallback: MatrixCallback diff --git a/vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt b/vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt deleted file mode 100644 index f24fc2a09c..0000000000 --- a/vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.utils - -import im.vector.matrix.android.api.MatrixCallback - -class NoOpMatrixCallback: MatrixCallback 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 91e8ab5300..0eee1eaccd 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 @@ -66,7 +66,7 @@ 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.LiveEvent -import im.vector.riotx.core.utils.NoOpMatrixCallback +import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand From a3ec0e03a04eeba15dc29c661642c999ad11fbaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Feb 2020 12:05:12 +0100 Subject: [PATCH 29/34] Use NoOpMatrixCallback when it's possible --- .../internal/crypto/DefaultCryptoService.kt | 3 ++- .../android/internal/task/ConfigurableTask.kt | 3 ++- .../settings/KeysBackupSettingsViewModel.kt | 3 ++- .../home/room/detail/RoomDetailViewModel.kt | 14 +++++++------- .../features/home/room/list/RoomListViewModel.kt | 3 ++- .../notifications/NotificationBroadcastReceiver.kt | 8 ++++---- 6 files changed, 19 insertions(+), 15 deletions(-) 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 7d6a4e8eff..a422394f16 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 @@ -26,6 +26,7 @@ import com.squareup.moshi.Types import com.zhuinden.monarchy.Monarchy import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.listeners.ProgressListener @@ -189,7 +190,7 @@ internal class DefaultCryptoService @Inject constructor( this.callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // bg refresh of crypto device - downloadKeys(listOf(credentials.userId), true, object : MatrixCallback> {}) + downloadKeys(listOf(credentials.userId), true, NoOpMatrixCallback()) callback.onSuccess(data) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt index 1f8cb5cc60..dce6f5f951 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.task import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.util.Cancelable import java.util.UUID @@ -47,7 +48,7 @@ internal data class ConfigurableTask( var callbackThread: TaskThread = TaskThread.MAIN, var executionThread: TaskThread = TaskThread.IO, var retryCount: Int = 0, - var callback: MatrixCallback = object : MatrixCallback {} + var callback: MatrixCallback = NoOpMatrixCallback() ) { fun build() = ConfigurableTask( diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt index bed18ae99d..e4bbd39684 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt @@ -19,6 +19,7 @@ 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.NoOpMatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState @@ -68,7 +69,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS } private fun init() { - keysBackupService.forceUsingLastVersion(object : MatrixCallback {}) + keysBackupService.forceUsingLastVersion(NoOpMatrixCallback()) } private fun getKeysBackupTrust() = withState { state -> 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 0eee1eaccd..6080ae6332 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 @@ -158,7 +158,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeUnreadState() observeMyRoomMember() room.getRoomSummaryLive() - room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, object : MatrixCallback {}) + room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) room.rx(session).loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() // Inform the SDK that the room is displayed session.onRoomDisplayed(initialState.roomId) @@ -225,7 +225,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun stopTrackingUnreadMessages() { if (trackUnreadMessages.getAndSet(false)) { mostRecentDisplayedEvent?.root?.eventId?.also { - room.setReadMarker(it, callback = object : MatrixCallback {}) + room.setReadMarker(it, callback = NoOpMatrixCallback()) } mostRecentDisplayedEvent = null } @@ -525,7 +525,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun popDraft() { - room.deleteDraft(object : MatrixCallback {}) + room.deleteDraft(NoOpMatrixCallback()) } private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { @@ -631,11 +631,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleRejectInvite() { - room.leave(null, object : MatrixCallback {}) + room.leave(null, NoOpMatrixCallback()) } private fun handleAcceptInvite() { - room.join(callback = object : MatrixCallback {}) + room.join(callback = NoOpMatrixCallback()) } private fun handleEditAction(action: RoomDetailAction.EnterEditMode) { @@ -808,14 +808,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId -> - room.setReadReceipt(eventId, callback = object : MatrixCallback {}) + room.setReadReceipt(eventId, callback = NoOpMatrixCallback()) } }) .disposeOnClear() } private fun handleMarkAllAsRead() { - room.markAsRead(ReadService.MarkAsReadParams.BOTH, object : MatrixCallback {}) + room.markAsRead(ReadService.MarkAsReadParams.BOTH, NoOpMatrixCallback()) } private fun handleReportContent(action: RoomDetailAction.ReportContent) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index 397df50c2b..22c18e9134 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -20,6 +20,7 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.NoOpMatrixCallback 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.model.RoomSummary @@ -182,7 +183,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, ?.filter { it.membership == Membership.JOIN } ?.map { it.roomId } ?.toList() - ?.let { session.markAllAsRead(it, object : MatrixCallback {}) } + ?.let { session.markAllAsRead(it, NoOpMatrixCallback()) } } private fun handleChangeNotificationMode(action: RoomListAction.ChangeRoomNotificationState) { diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt index 816e0dc0ad..53d69bce2d 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt @@ -20,7 +20,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.core.app.RemoteInput -import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.read.ReadService @@ -75,21 +75,21 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { private fun handleJoinRoom(roomId: String) { activeSessionHolder.getSafeActiveSession()?.let { session -> session.getRoom(roomId) - ?.join(callback = object : MatrixCallback {}) + ?.join(callback = NoOpMatrixCallback()) } } private fun handleRejectRoom(roomId: String) { activeSessionHolder.getSafeActiveSession()?.let { session -> session.getRoom(roomId) - ?.leave(callback = object : MatrixCallback {}) + ?.leave(callback = NoOpMatrixCallback()) } } private fun handleMarkAsRead(roomId: String) { activeSessionHolder.getActiveSession().let { session -> session.getRoom(roomId) - ?.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, object : MatrixCallback {}) + ?.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) } } From f72e5c1d9481c642a75402bac1ec0d94524bd697 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Feb 2020 12:16:43 +0100 Subject: [PATCH 30/34] Explain why and when to use EllipsizingTextView --- .../java/im/vector/riotx/core/platform/EllipsizingTextView.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt index a0956d0528..9d119f4063 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt @@ -43,6 +43,8 @@ import java.util.regex.Pattern /* * Imported from https://gist.github.com/hateum/d2095575b441007d62b8 + * + * Use it in your layout to avoid this issue: https://issuetracker.google.com/issues/121092510 */ /** From 3384d91adb7019dc45100e4431e9252c2b472659 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Feb 2020 12:39:26 +0100 Subject: [PATCH 31/34] Rename MessageContent.type to MessageContent.msgType for code clarity and update a few the Javadoc --- .../android/api/session/events/model/Event.kt | 4 ++-- .../room/model/message/MessageAudioContent.kt | 6 ++--- .../room/model/message/MessageContent.kt | 3 +-- .../model/message/MessageDefaultContent.kt | 2 +- .../room/model/message/MessageEmoteContent.kt | 18 ++++++++++++++- .../model/message/MessageEncryptedContent.kt | 5 +++- .../room/model/message/MessageFileContent.kt | 9 +++++--- .../room/model/message/MessageImageContent.kt | 4 ++-- .../model/message/MessageLocationContent.kt | 4 ++-- .../model/message/MessageNoticeContent.kt | 18 ++++++++++++++- .../model/message/MessageStickerContent.kt | 5 ++-- .../room/model/message/MessageTextContent.kt | 18 ++++++++++++++- .../MessageVerificationRequestContent.kt | 2 +- .../room/model/message/MessageVideoContent.kt | 4 ++-- .../tasks/RoomVerificationUpdateTask.kt | 4 ++-- .../DefaultVerificationService.kt | 2 +- .../room/send/LocalEchoEventFactory.kt | 23 ++++++++++--------- .../session/room/send/LocalEchoRepository.kt | 6 ++--- .../internal/session/room/send/TextContent.kt | 2 +- .../session/sync/CryptoSyncHandler.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../action/MessageActionsViewModel.kt | 22 +++++++++--------- .../timeline/factory/MessageItemFactory.kt | 4 ++-- .../format/DisplayableEventFormatter.kt | 2 +- 24 files changed, 112 insertions(+), 59 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 553c36a9f4..fb94d61c0b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -200,7 +200,7 @@ data class Event( fun Event.isTextMessage(): Boolean { return getClearType() == EventType.MESSAGE - && when (getClearContent()?.toModel()?.type) { + && when (getClearContent()?.toModel()?.msgType) { MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_NOTICE -> true @@ -210,7 +210,7 @@ fun Event.isTextMessage(): Boolean { fun Event.isImageMessage(): Boolean { return getClearType() == EventType.MESSAGE - && when (getClearContent()?.toModel()?.type) { + && when (getClearContent()?.toModel()?.msgType) { MessageType.MSGTYPE_IMAGE -> true else -> false } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt index 624d827de1..e9c6c71882 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt @@ -25,9 +25,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class MessageAudioContent( /** - * Not documented + * Required. Must be 'm.audio'. */ - @Json(name = "msgtype") override val type: String, + @Json(name = "msgtype") override val msgType: String, /** * Required. A description of the audio e.g. 'Bee Gees - Stayin' Alive', or some kind of content description for accessibility e.g. 'audio attachment'. @@ -40,7 +40,7 @@ data class MessageAudioContent( @Json(name = "info") val audioInfo: AudioInfo? = null, /** - * Required. Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip. + * Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip. */ @Json(name = "url") override val url: String? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt index c0adfa4c36..bc5228e5e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt @@ -20,8 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent interface MessageContent { - // TODO Rename to msgType - val type: String + val msgType: String val body: String val relatesTo: RelationDefaultContent? val newContent: Content? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt index 705dbf5d85..7a5c7e2cd5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt @@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC @JsonClass(generateAdapter = true) data class MessageDefaultContent( - @Json(name = "msgtype") override val type: String, + @Json(name = "msgtype") override val msgType: String, @Json(name = "body") override val body: String, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt index ec340659a2..e7106a9755 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt @@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC @JsonClass(generateAdapter = true) data class MessageEmoteContent( - @Json(name = "msgtype") override val type: String, + /** + * Required. Must be 'm.emote'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. The emote action to perform. + */ @Json(name = "body") override val body: String, + + /** + * The format used in the formatted_body. Currently only org.matrix.custom.html is supported. + */ @Json(name = "format") val format: String? = null, + + /** + * The formatted version of the body. This is required if format is specified. + */ @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt index 110b9c68f0..1d1d01c09c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncryptedContent.kt @@ -23,10 +23,13 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo */ interface MessageEncryptedContent : MessageContent { /** - * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image. + * Required if the file is unencrypted. The URL (typically MXC URI) to the image. */ val url: String? + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ val encryptedFileInfo: EncryptedFileInfo? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt index ac70f5bf74..7c635a401d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt @@ -26,9 +26,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class MessageFileContent( /** - * Not documented + * Required. Must be 'm.file'. */ - @Json(name = "msgtype") override val type: String, + @Json(name = "msgtype") override val msgType: String, /** * Required. A human-readable description of the file. This is recommended to be the filename of the original upload. @@ -46,13 +46,16 @@ data class MessageFileContent( @Json(name = "info") val info: FileInfo? = null, /** - * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the file. + * Required if the file is unencrypted. The URL (typically MXC URI) to the file. */ @Json(name = "url") override val url: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null, + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null ) : MessageEncryptedContent { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt index fbac261802..f50a108947 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt @@ -27,7 +27,7 @@ data class MessageImageContent( /** * Required. Must be 'm.image'. */ - @Json(name = "msgtype") override val type: String, + @Json(name = "msgtype") override val msgType: String, /** * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image, @@ -41,7 +41,7 @@ data class MessageImageContent( @Json(name = "info") override val info: ImageInfo? = null, /** - * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image. + * Required if the file is unencrypted. The URL (typically MXC URI) to the image. */ @Json(name = "url") override val url: String? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt index 15c746fd38..02ba9eae6e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt @@ -24,9 +24,9 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC @JsonClass(generateAdapter = true) data class MessageLocationContent( /** - * Not documented + * Required. Must be 'm.location'. */ - @Json(name = "msgtype") override val type: String, + @Json(name = "msgtype") override val msgType: String, /** * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt index 9a4fea1060..e08e07e9da 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt @@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC @JsonClass(generateAdapter = true) data class MessageNoticeContent( - @Json(name = "msgtype") override val type: String, + /** + * Required. Must be 'm.notice'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. The notice text to send. + */ @Json(name = "body") override val body: String, + + /** + * The format used in the formatted_body. Currently only org.matrix.custom.html is supported. + */ @Json(name = "format") val format: String? = null, + + /** + * The formatted version of the body. This is required if format is specified. + */ @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt index d1b4a5c3cb..9198537bff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt @@ -12,7 +12,6 @@ * 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.message @@ -28,7 +27,7 @@ data class MessageStickerContent( /** * Set in local, not from server */ - override val type: String = MessageType.MSGTYPE_STICKER_LOCAL, + override val msgType: String = MessageType.MSGTYPE_STICKER_LOCAL, /** * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image, @@ -42,7 +41,7 @@ data class MessageStickerContent( @Json(name = "info") override val info: ImageInfo? = null, /** - * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image. + * Required if the file is unencrypted. The URL (typically MXC URI) to the image. */ @Json(name = "url") override val url: String? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt index 001b9ff0dd..cc5bb1f774 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt @@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC @JsonClass(generateAdapter = true) data class MessageTextContent( - @Json(name = "msgtype") override val type: String, + /** + * Required. Must be 'm.text'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. The body of the message. + */ @Json(name = "body") override val body: String, + + /** + * The format used in the formatted_body. Currently only org.matrix.custom.html is supported. + */ @Json(name = "format") val format: String? = null, + + /** + * The formatted version of the body. This is required if format is specified. + */ @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt index c8026e2f26..2ef4979f9d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReq @JsonClass(generateAdapter = true) data class MessageVerificationRequestContent( - @Json(name = "msgtype") override val type: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, + @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, @Json(name = "body") override val body: String, @Json(name = "from_device") override val fromDevice: String?, @Json(name = "methods") override val methods: List, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt index 1c84b3e72e..4cf03a5ffd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt @@ -27,7 +27,7 @@ data class MessageVideoContent( /** * Required. Must be 'm.video'. */ - @Json(name = "msgtype") override val type: String, + @Json(name = "msgtype") override val msgType: String, /** * Required. A description of the video e.g. 'Gangnam style', or some kind of content description for accessibility e.g. 'video attachment'. @@ -40,7 +40,7 @@ data class MessageVideoContent( @Json(name = "info") val videoInfo: VideoInfo? = null, /** - * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the video clip. + * Required if the file is unencrypted. The URL (typically MXC URI) to the video clip. */ @Json(name = "url") override val url: String? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt index 5a8f8e7ba5..d6118867ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -88,7 +88,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( // done from another device of mine if (EventType.MESSAGE == event.type) { - val msgType = event.getClearContent().toModel()?.type + val msgType = event.getClearContent().toModel()?.msgType if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { event.getClearContent().toModel()?.let { if (it.fromDevice != deviceId) { @@ -144,7 +144,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( params.verificationService.onRoomEvent(event) } EventType.MESSAGE -> { - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { params.verificationService.onRoomRequestReceived(event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index 911fc2a572..fc14b0a23a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -165,7 +165,7 @@ internal class DefaultVerificationService @Inject constructor( onRoomDoneReceived(event) } EventType.MESSAGE -> { - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { onRoomRequestReceived(event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 9deb79136d..5d7a7c085f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -83,7 +83,7 @@ internal class LocalEchoEventFactory @Inject constructor( if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType) } - val content = MessageTextContent(type = msgType, body = text.toString()) + val content = MessageTextContent(msgType = msgType, body = text.toString()) return createEvent(roomId, content) } @@ -122,7 +122,7 @@ internal class LocalEchoEventFactory @Inject constructor( compatibilityText: String): Event { return createEvent(roomId, MessageTextContent( - type = msgType, + msgType = msgType, body = compatibilityText, relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), newContent = createTextContent(newBodyText, newBodyAutoMarkdown) @@ -131,7 +131,8 @@ internal class LocalEchoEventFactory @Inject constructor( )) } - fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent, + fun createReplaceTextOfReply(roomId: String, + eventReplaced: TimelineEvent, originalEvent: TimelineEvent, newBodyText: String, newBodyAutoMarkdown: Boolean, @@ -157,11 +158,11 @@ internal class LocalEchoEventFactory @Inject constructor( return createEvent(roomId, MessageTextContent( - type = msgType, + msgType = msgType, body = compatibilityText, relatesTo = RelationDefaultContent(RelationType.REPLACE, eventReplaced.root.eventId), newContent = MessageTextContent( - type = msgType, + msgType = msgType, format = MessageType.FORMAT_MATRIX_HTML, body = replyFallback, formattedBody = replyFormatted @@ -214,7 +215,7 @@ internal class LocalEchoEventFactory @Inject constructor( } val content = MessageImageContent( - type = MessageType.MSGTYPE_IMAGE, + msgType = MessageType.MSGTYPE_IMAGE, body = attachment.name ?: "image", info = ImageInfo( mimeType = attachment.mimeType, @@ -246,7 +247,7 @@ internal class LocalEchoEventFactory @Inject constructor( ) } val content = MessageVideoContent( - type = MessageType.MSGTYPE_VIDEO, + msgType = MessageType.MSGTYPE_VIDEO, body = attachment.name ?: "video", videoInfo = VideoInfo( mimeType = attachment.mimeType, @@ -265,7 +266,7 @@ internal class LocalEchoEventFactory @Inject constructor( private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { val content = MessageAudioContent( - type = MessageType.MSGTYPE_AUDIO, + msgType = MessageType.MSGTYPE_AUDIO, body = attachment.name ?: "audio", audioInfo = AudioInfo( mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg", @@ -278,7 +279,7 @@ internal class LocalEchoEventFactory @Inject constructor( private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event { val content = MessageFileContent( - type = MessageType.MSGTYPE_FILE, + msgType = MessageType.MSGTYPE_FILE, body = attachment.name ?: "file", info = FileInfo( mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } @@ -349,7 +350,7 @@ internal class LocalEchoEventFactory @Inject constructor( val eventId = eventReplied.root.eventId ?: return null val content = MessageTextContent( - type = MessageType.MSGTYPE_TEXT, + msgType = MessageType.MSGTYPE_TEXT, format = MessageType.FORMAT_MATRIX_HTML, body = replyFallback, formattedBody = replyFormatted, @@ -383,7 +384,7 @@ internal class LocalEchoEventFactory @Inject constructor( * himself a reply, but it will contain the fallbacks, so we have to trim them. */ private fun bodyForReply(content: MessageContent?, originalContent: MessageContent?): TextContent { - when (content?.type) { + when (content?.msgType) { MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE -> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt index c924e891c8..fa43dde95e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -116,7 +116,7 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon EventType.REACTION -> { val content = event.getClearContent().toModel() if (content != null) { - when (content.type) { + when (content.msgType) { MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_LOCATION, @@ -127,11 +127,11 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_AUDIO -> { - // need to resend the attachement + // need to resend the attachment false } else -> { - Timber.e("Cannot resend message ${event.type} / ${content.type}") + Timber.e("Cannot resend message ${event.type} / ${content.msgType}") false } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt index 27b68e95eb..ae973b17f5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt @@ -33,7 +33,7 @@ data class TextContent( fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent { return MessageTextContent( - type = msgType, + msgType = msgType, format = MessageType.FORMAT_MATRIX_HTML.takeIf { formattedText != null }, body = text, formattedBody = formattedText diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt index bda3623078..10e7ceb692 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt @@ -41,7 +41,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: // Decrypt event if necessary decryptEvent(event, null) if (event.getClearType() == EventType.MESSAGE - && event.getClearContent()?.toModel()?.type == "m.bad.encrypted") { + && event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") } else { verificationService.onToDeviceEvent(event) 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 6080ae6332..c58270aff2 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 @@ -480,7 +480,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", - messageContent?.type ?: MessageType.MSGTYPE_TEXT, + messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) } else { 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 aa81658bf9..8e175721ab 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 @@ -203,7 +203,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun actionsForEvent(timelineEvent: TimelineEvent): List { val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() ?: timelineEvent.root.getClearContent().toModel() - val type = messageContent?.type + val msgType = messageContent?.msgType return arrayListOf().apply { if (timelineEvent.root.sendState.hasFailed()) { @@ -230,7 +230,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.Delete(eventId)) } - if (canCopy(type)) { + if (canCopy(msgType)) { // TODO copy images? html? see ClipBoard add(EventSharedAction.Copy(messageContent!!.body)) } @@ -251,7 +251,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.ViewEditHistory(informationData)) } - if (canShare(type)) { + if (canShare(msgType)) { if (messageContent is MessageImageContent) { session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url -> add(EventSharedAction.Share(url)) @@ -296,7 +296,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false - return when (messageContent?.type) { + return when (messageContent?.msgType) { MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_EMOTE, @@ -311,7 +311,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false - return when (messageContent?.type) { + return when (messageContent?.msgType) { MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_EMOTE, @@ -347,13 +347,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted // TODO if user is admin or moderator val messageContent = event.root.getClearContent().toModel() return event.root.senderId == myUserId && ( - messageContent?.type == MessageType.MSGTYPE_TEXT - || messageContent?.type == MessageType.MSGTYPE_EMOTE + messageContent?.msgType == MessageType.MSGTYPE_TEXT + || messageContent?.msgType == MessageType.MSGTYPE_EMOTE ) } - private fun canCopy(type: String?): Boolean { - return when (type) { + private fun canCopy(msgType: String?): Boolean { + return when (msgType) { MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_EMOTE, @@ -363,8 +363,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - private fun canShare(type: String?): Boolean { - return when (type) { + private fun canShare(msgType: String?): Boolean { + return when (msgType) { MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_VIDEO -> true diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 086dfe3754..65a6f5f244 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -229,7 +229,7 @@ class MessageItemFactory @Inject constructor( informationData: MessageInformationData, highlight: Boolean, callback: TimelineEventController.Callback?): DefaultItem? { - val text = stringProvider.getString(R.string.rendering_event_error_type_of_message_not_handled, messageContent.type) + val text = stringProvider.getString(R.string.rendering_event_error_type_of_message_not_handled, messageContent.msgType) return defaultItemFactory.create(text, informationData, highlight, callback) } @@ -258,7 +258,7 @@ class MessageItemFactory @Inject constructor( .highlighted(highlight) .mediaData(data) .apply { - if (messageContent.type == MessageType.MSGTYPE_STICKER_LOCAL) { + if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) { mode(ImageContentRenderer.Mode.STICKER) } else { clickListener( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index ed6bc9df62..294429368a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -46,7 +46,7 @@ class DisplayableEventFormatter @Inject constructor( when (timelineEvent.root.getClearType()) { EventType.MESSAGE -> { timelineEvent.getLastMessageContent()?.let { messageContent -> - when (messageContent.type) { + when (messageContent.msgType) { MessageType.MSGTYPE_VERIFICATION_REQUEST -> { return simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor) } From 05dd587fa8927da0c2990bbbb7711d4b78ec4bdf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Feb 2020 12:44:31 +0100 Subject: [PATCH 32/34] Move FORMAT_MATRIX_HTML to a proper object and so fix a wrong usage issue --- .../room/model/message/MessageFormat.kt | 21 +++++++++++++++++++ .../session/room/model/message/MessageType.kt | 2 -- .../room/send/LocalEchoEventFactory.kt | 7 ++++--- .../internal/session/room/send/TextContent.kt | 3 ++- .../home/room/detail/RoomDetailFragment.kt | 4 ++-- .../action/MessageActionsViewModel.kt | 4 +--- 6 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFormat.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFormat.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFormat.kt new file mode 100644 index 0000000000..3effa9a630 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFormat.kt @@ -0,0 +1,21 @@ +/* + * 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.model.message + +object MessageFormat { + const val FORMAT_MATRIX_HTML = "org.matrix.custom.html" +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt index d4e6d5ea71..2707283325 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.api.session.room.model.message object MessageType { - const val MSGTYPE_TEXT = "m.text" const val MSGTYPE_EMOTE = "m.emote" const val MSGTYPE_NOTICE = "m.notice" @@ -27,7 +26,6 @@ object MessageType { const val MSGTYPE_LOCATION = "m.location" const val MSGTYPE_FILE = "m.file" const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" - const val FORMAT_MATRIX_HTML = "org.matrix.custom.html" // Add, in local, a fake message type in order to StickerMessage can inherit Message class // Because sticker isn't a message type but a event type without msgtype field const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 5d7a7c085f..bfa1d380ae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.room.model.message.ImageInfo import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageFormat import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType @@ -163,7 +164,7 @@ internal class LocalEchoEventFactory @Inject constructor( relatesTo = RelationDefaultContent(RelationType.REPLACE, eventReplaced.root.eventId), newContent = MessageTextContent( msgType = msgType, - format = MessageType.FORMAT_MATRIX_HTML, + format = MessageFormat.FORMAT_MATRIX_HTML, body = replyFallback, formattedBody = replyFormatted ) @@ -351,7 +352,7 @@ internal class LocalEchoEventFactory @Inject constructor( val eventId = eventReplied.root.eventId ?: return null val content = MessageTextContent( msgType = MessageType.MSGTYPE_TEXT, - format = MessageType.FORMAT_MATRIX_HTML, + format = MessageFormat.FORMAT_MATRIX_HTML, body = replyFallback, formattedBody = replyFormatted, relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) @@ -390,7 +391,7 @@ internal class LocalEchoEventFactory @Inject constructor( MessageType.MSGTYPE_NOTICE -> { var formattedText: String? = null if (content is MessageTextContent) { - if (content.format == MessageType.FORMAT_MATRIX_HTML) { + if (content.format == MessageFormat.FORMAT_MATRIX_HTML) { formattedText = content.formattedBody } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt index ae973b17f5..5058c1991f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.send +import im.vector.matrix.android.api.session.room.model.message.MessageFormat 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.api.util.ContentUtils.extractUsefulTextFromHtmlReply @@ -34,7 +35,7 @@ data class TextContent( fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent { return MessageTextContent( msgType = msgType, - format = MessageType.FORMAT_MATRIX_HTML.takeIf { formattedText != null }, + format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null }, body = text, formattedBody = formattedText ) 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 e09270b52b..8645607dae 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 @@ -72,9 +72,9 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageFormat import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent 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.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.send.SendState @@ -448,7 +448,7 @@ class RoomDetailFragment @Inject constructor( val messageContent: MessageContent? = event.getLastMessageContent() val nonFormattedBody = messageContent?.body ?: "" var formattedBody: CharSequence? = null - if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { + if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) 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 8e175721ab..dcaddd3c08 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 @@ -172,7 +172,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.MESSAGE, EventType.STICKER -> { val messageContent: MessageContent? = timelineEvent.getLastMessageContent() - if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { + if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { val html = messageContent.formattedBody ?.takeIf { it.isNotBlank() } ?.let { htmlCompressor.compress(it) } @@ -315,7 +315,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_EMOTE, - MessageType.FORMAT_MATRIX_HTML, MessageType.MSGTYPE_LOCATION -> { true } @@ -357,7 +356,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_EMOTE, - MessageType.FORMAT_MATRIX_HTML, MessageType.MSGTYPE_LOCATION -> true else -> false } From dcd7d17ffd0b2403ea1fe68e79bb4cadb2cebf17 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Feb 2020 14:13:43 +0100 Subject: [PATCH 33/34] Fix compilation issue after merge and update CHANGES.md --- CHANGES.md | 3 ++- .../matrix/android/internal/session/SessionModule.kt | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 19f4f88b76..e0a122ca69 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,8 @@ Features ✨: - Improvements 🙌: - - + - Improve navigation to the timeline (#789, #862) + - Improve network detection. It is now based on the sync request status (#873, #882) Other changes: - 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 43aa39ee2f..969a968a91 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 @@ -24,10 +24,12 @@ import dagger.Lazy import dagger.Module import dagger.Provides import dagger.multibindings.IntoSet +import im.vector.matrix.android.api.MatrixConfiguration 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.api.auth.data.sessionId +import im.vector.matrix.android.api.crypto.MXCryptoConfig 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 @@ -214,6 +216,13 @@ internal abstract class SessionModule { fallbackNetworkCallbackStrategy.get() } } + + @JvmStatic + @Provides + @SessionScope + fun providesMxCryptoConfig(matrixConfiguration: MatrixConfiguration): MXCryptoConfig { + return matrixConfiguration.cryptoConfig + } } @Binds From 50814dafe90cf7598a3705ce88322a449c36398b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Feb 2020 14:19:04 +0100 Subject: [PATCH 34/34] Update wording --- vector/src/main/res/layout/view_sync_state.xml | 3 ++- vector/src/main/res/values/strings_riotX.xml | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/layout/view_sync_state.xml b/vector/src/main/res/layout/view_sync_state.xml index 00679bec15..bc828045fe 100644 --- a/vector/src/main/res/layout/view_sync_state.xml +++ b/vector/src/main/res/layout/view_sync_state.xml @@ -21,6 +21,7 @@ android:layout_gravity="center" android:background="?riotx_header_panel_background" android:indeterminate="true" /> + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index d093f2b4d5..ff178f067b 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -123,7 +123,7 @@ Cross-Signing Cross-Signing is enabled\nPrivate Keys on device. - Cross-Signing is enabled\nKeys are trusted.\nPrivate keys are not known + Cross-Signing is enabled\nKeys are trusted.\nPrivate keys are not known Cross-Signing is enabled.\nKeys are not trusted Cross-Signing is not enabled @@ -172,4 +172,6 @@ Did the other user successfully scan the QR code? Yes No + + Connectivity to the server has been lost