From 5338f93852101b2cef3a7a457e781fad86a3ae10 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Dec 2019 19:52:12 +0100 Subject: [PATCH 001/105] Sync: use a foreground service for initialSync. --- .../matrix/android/api/failure/Failure.kt | 2 + .../matrix/android/api/session/Session.kt | 5 + .../android/api/session/cache/CacheService.kt | 2 +- .../network/NetworkConnectivityChecker.kt | 29 +++- .../internal/session/DefaultSession.kt | 18 +-- .../internal/session/SessionComponent.kt | 3 + .../android/internal/session/sync/SyncTask.kt | 4 + .../internal/session/sync/job/SyncService.kt | 134 ++++++------------ .../internal/session/sync/job/SyncThread.kt | 3 +- .../internal/session/sync/job/SyncWorker.kt | 42 ++++-- vector/src/fdroid/AndroidManifest.xml | 4 - .../receiver/AlarmSyncBroadcastReceiver.kt | 12 +- vector/src/main/AndroidManifest.xml | 4 + .../java/im/vector/riotx/VectorApplication.kt | 3 +- .../vector/riotx/core/extensions/Session.kt | 29 +++- .../riotx/core/services}/VectorSyncService.kt | 15 +- .../im/vector/riotx/features/MainActivity.kt | 12 +- .../riotx/features/login/LoginViewModel.kt | 4 +- 18 files changed, 177 insertions(+), 148 deletions(-) rename vector/src/{fdroid/java/im/vector/riotx/fdroid/service => main/java/im/vector/riotx/core/services}/VectorSyncService.kt (83%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt index 4d44e3346b..41468e9e0f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt @@ -44,3 +44,5 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { abstract class FeatureFailure : Failure() } + +fun Throwable.isTokenError() = this is Failure.ServerError && (this.error.code == MatrixError.UNKNOWN_TOKEN || this.error.code == MatrixError.MISSING_TOKEN) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 2440713a40..5fd7e3885f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -104,6 +104,11 @@ interface Session : */ fun syncState(): LiveData + /** + * This methods return true if an initial sync has been processed + */ + fun hasAlreadySynced(): Boolean + /** * This method allow to close a session. It does stop some services. */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt index a84e5af48c..2f34922280 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.api.MatrixCallback interface CacheService { /** - * Clear the whole cached data, except credentials. Once done, the session is closed and has to be opened again + * Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user. */ fun clearCache(callback: MatrixCallback) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index 3d850c223a..4d4699810f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.network import android.content.Context +import androidx.annotation.WorkerThread import com.novoda.merlin.Merlin import com.novoda.merlin.MerlinsBeard import im.vector.matrix.android.internal.di.MatrixScope @@ -28,8 +29,8 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @MatrixScope -internal class NetworkConnectivityChecker @Inject constructor(context: Context, - backgroundDetectionObserver: BackgroundDetectionObserver) +internal class NetworkConnectivityChecker @Inject constructor(private val context: Context, + private val backgroundDetectionObserver: BackgroundDetectionObserver) : BackgroundDetectionObserver.Listener { private val merlin = Merlin.Builder() @@ -37,19 +38,30 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context, .withDisconnectableCallbacks() .build(context) - private val listeners = Collections.synchronizedSet(LinkedHashSet()) + private val merlinsBeard = MerlinsBeard.Builder().build(context) - // True when internet is available - var hasInternetAccess = MerlinsBeard.Builder().build(context).isConnected - private set + private val listeners = Collections.synchronizedSet(LinkedHashSet()) + private var hasInternetAccess = merlinsBeard.isConnected init { backgroundDetectionObserver.register(this) } + /** + * Returns true when internet is available + */ + @WorkerThread + fun hasInternetAccess(): Boolean { + // If we are in background we have unbound merlin, so we have to check + return if (backgroundDetectionObserver.isIsBackground) { + merlinsBeard.hasInternetAccess() + } else { + hasInternetAccess + } + } + override fun onMoveToForeground() { merlin.bind() - merlin.registerDisconnectable { if (hasInternetAccess) { Timber.v("On Disconnect") @@ -76,14 +88,17 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context, merlin.unbind() } + // In background you won't get notification as merlin is unbound suspend fun waitUntilConnected() { if (hasInternetAccess) { return } else { + Timber.v("Waiting for network...") suspendCoroutine { continuation -> register(object : Listener { override fun onConnect() { unregister(this) + Timber.v("Connected to network...") continuation.resume(Unit) } }) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 4b33e28000..32126fb78d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -44,6 +44,7 @@ import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.database.LiveEntityObserver +import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncWorker import org.greenrobot.eventbus.EventBus @@ -72,6 +73,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val secureStorageService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, + private val syncTokenStore: SyncTokenStore, private val contentUploadProgressTracker: ContentUploadStateTracker, private val initialSyncProgressService: Lazy, private val homeServerCapabilitiesService: Lazy) @@ -147,6 +149,10 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se return getSyncThread().liveState() } + override fun hasAlreadySynced(): Boolean { + return syncTokenStore.getLastToken() != null + } + private fun getSyncThread(): SyncThread { return syncThread ?: syncThreadProvider.get().also { syncThread = it @@ -156,17 +162,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se override fun clearCache(callback: MatrixCallback) { stopSync() stopAnyBackgroundSync() - cacheService.get().clearCache(object : MatrixCallback { - override fun onSuccess(data: Unit) { - startSync(true) - callback.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - startSync(true) - callback.onFailure(failure) - } - }) + cacheService.get().clearCache(callback) } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 8c16050442..a208a7a720 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -46,6 +46,7 @@ import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.user.UserModule import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @Component(dependencies = [MatrixComponent::class], modules = [ @@ -69,6 +70,8 @@ import im.vector.matrix.android.internal.task.TaskExecutor @SessionScope internal interface SessionComponent { + fun coroutineDispatchers(): MatrixCoroutineDispatchers + fun session(): Session fun syncTask(): SyncTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 51c02456d7..1082fa835b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.internal.auth.SessionParamsStore 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.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.filter.FilterRepository @@ -28,6 +29,7 @@ import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabil import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.session.user.UserStore import im.vector.matrix.android.internal.task.Task +import timber.log.Timber import javax.inject.Inject internal interface SyncTask : Task { @@ -47,6 +49,7 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, ) : SyncTask { override suspend fun execute(params: SyncTask.Params) { + Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") // Maybe refresh the home server capabilities data we know getHomeServerCapabilitiesTask.execute(Unit) @@ -84,5 +87,6 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, if (isInitialSync) { initialSyncProgressService.endAll() } + Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}") } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt index 4e57aa5be1..3aeb408db1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt @@ -18,21 +18,15 @@ package im.vector.matrix.android.internal.session.sync.job import android.app.Service import android.content.Intent import android.os.IBinder -import com.squareup.moshi.JsonEncodingException import im.vector.matrix.android.api.Matrix -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.failure.MatrixError -import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.failure.isTokenError import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.TaskThread -import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.* import timber.log.Timber -import java.net.SocketTimeoutException -import java.util.Timer -import java.util.TimerTask +import java.util.concurrent.atomic.AtomicBoolean /** * Can execute periodic sync task. @@ -43,13 +37,15 @@ import java.util.TimerTask open class SyncService : Service() { private var mIsSelfDestroyed: Boolean = false - private var cancelableTask: Cancelable? = null private lateinit var syncTask: SyncTask private lateinit var networkConnectivityChecker: NetworkConnectivityChecker private lateinit var taskExecutor: TaskExecutor + private lateinit var coroutineDispatchers: MatrixCoroutineDispatchers - var timer = Timer() + private val isRunning = AtomicBoolean(false) + + private val serviceScope = CoroutineScope(SupervisorJob()) override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Timber.i("onStartCommand $intent") @@ -60,13 +56,14 @@ open class SyncService : Service() { syncTask = sessionComponent.syncTask() networkConnectivityChecker = sessionComponent.networkConnectivityChecker() taskExecutor = sessionComponent.taskExecutor() - if (cancelableTask == null) { - timer.cancel() - timer = Timer() - doSync(true) + coroutineDispatchers = sessionComponent.coroutineDispatchers() + if (isRunning.get()) { + Timber.i("Received a start while was already syncing... ignore") } else { - // Already syncing ignore - Timber.i("Received a start while was already syncking... ignore") + isRunning.set(true) + serviceScope.launch(coroutineDispatchers.sync) { + doSync() + } } } // No intent just start the service, an alarm will should call with intent @@ -75,86 +72,40 @@ open class SyncService : Service() { override fun onDestroy() { Timber.i("## onDestroy() : $this") - if (!mIsSelfDestroyed) { Timber.w("## Destroy by the system : $this") } - - cancelableTask?.cancel() + serviceScope.coroutineContext.cancelChildren() + isRunning.set(false) super.onDestroy() } - fun stopMe() { - timer.cancel() - timer = Timer() - cancelableTask?.cancel() + private fun stopMe() { mIsSelfDestroyed = true stopSelf() } - fun doSync(once: Boolean = false) { - if (!networkConnectivityChecker.hasInternetAccess) { - Timber.v("No internet access. Waiting...") - // TODO Retry in ? - timer.schedule(object : TimerTask() { - override fun run() { - doSync() - } - }, NO_NETWORK_DELAY) - } else { - Timber.v("Execute sync request with timeout 0") - val params = SyncTask.Params(TIME_OUT) - cancelableTask = syncTask - .configureWith(params) { - callbackThread = TaskThread.SYNC - executionThread = TaskThread.SYNC - callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - cancelableTask = null - if (!once) { - timer.schedule(object : TimerTask() { - override fun run() { - doSync() - } - }, NEXT_BATCH_DELAY) - } else { - // stop - stopMe() - } - } - - override fun onFailure(failure: Throwable) { - Timber.e(failure) - cancelableTask = null - if (failure is Failure.NetworkConnection - && failure.cause is SocketTimeoutException) { - // Timeout are not critical - timer.schedule(object : TimerTask() { - override fun run() { - doSync() - } - }, 5_000L) - } - - if (failure !is Failure.NetworkConnection - || failure.cause is JsonEncodingException) { - // Wait 10s before retrying - timer.schedule(object : TimerTask() { - override fun run() { - doSync() - } - }, 5_000L) - } - - if (failure is Failure.ServerError - && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { - // No token or invalid token, stop the thread - stopSelf() - } - } - } - } - .executeBy(taskExecutor) + private suspend fun doSync() { + if (!networkConnectivityChecker.hasInternetAccess()) { + Timber.v("No network, try to sync again in 10s") + delay(DELAY_NO_NETWORK) + doSync() + return + } + Timber.v("Execute sync request with timeout 0") + val params = SyncTask.Params(TIME_OUT) + try { + syncTask.execute(params) + stopMe() + } catch (throwable: Throwable) { + Timber.e(throwable) + if (throwable.isTokenError()) { + stopMe() + } else { + Timber.v("Retry to sync in 5s") + delay(DELAY_FAILURE) + doSync() + } } } @@ -164,9 +115,8 @@ open class SyncService : Service() { companion object { const val EXTRA_USER_ID = "EXTRA_USER_ID" - - const val TIME_OUT = 0L - const val NEXT_BATCH_DELAY = 60_000L - const val NO_NETWORK_DELAY = 5_000L + private const val TIME_OUT = 0L + private const val DELAY_NO_NETWORK = 10_000L + 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 d8de292d70..67169cdc3c 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 @@ -99,11 +99,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, isStarted = true networkConnectivityChecker.register(this) backgroundDetectionObserver.register(this) - while (state != SyncState.KILLING) { Timber.v("Entering loop, state: $state") - if (!networkConnectivityChecker.hasInternetAccess) { + if (!networkConnectivityChecker.hasInternetAccess()) { Timber.v("No network. Waiting...") updateStateTo(SyncState.NO_NETWORK) synchronized(lock) { lock.wait() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt index b5177172d0..804102913d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt @@ -18,6 +18,10 @@ package im.vector.matrix.android.internal.session.sync.job import android.content.Context import androidx.work.* import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.failure.isTokenError +import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @@ -25,6 +29,7 @@ import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.TimeUnit @@ -46,45 +51,58 @@ internal class SyncWorker(context: Context, @Inject lateinit var syncTask: SyncTask @Inject lateinit var taskExecutor: TaskExecutor @Inject lateinit var coroutineDispatchers: MatrixCoroutineDispatchers + @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker override suspend fun doWork(): Result { Timber.i("Sync work starting") val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() sessionComponent.inject(this) - runCatching { - withContext(coroutineDispatchers.sync) { - val taskParams = SyncTask.Params(0) - syncTask.execute(taskParams) - } - } - return Result.success() + return runCatching { + doSync(params.timeout) + }.fold( + { Result.success() }, + { failure -> + if (failure.isTokenError() || !params.automaticallyRetry) { + Result.failure() + } else { + Result.retry() + } + } + ) + } + + private suspend fun doSync(timeout: Long) = withContext(coroutineDispatchers.sync) { + val taskParams = SyncTask.Params(timeout) + syncTask.execute(taskParams) } companion object { + const val BG_SYNC_WORK_NAME = "BG_SYNCP" + fun requireBackgroundSync(context: Context, userId: String, serverTimeout: Long = 0) { val data = WorkerParamsFactory.toData(Params(userId, serverTimeout, false)) val workRequest = matrixOneTimeWorkRequestBuilder() - .setInputData(data) .setConstraints(WorkManagerUtil.workConstraints) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) + .setInputData(data) .build() - WorkManager.getInstance(context).enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest) + WorkManager.getInstance(context).enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) } fun automaticallyBackgroundSync(context: Context, userId: String, serverTimeout: Long = 0, delay: Long = 30_000) { val data = WorkerParamsFactory.toData(Params(userId, serverTimeout, true)) val workRequest = matrixOneTimeWorkRequestBuilder() - .setInputData(data) .setConstraints(WorkManagerUtil.workConstraints) + .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS) .build() - WorkManager.getInstance(context).enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest) + WorkManager.getInstance(context).enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) } fun stopAnyBackgroundSync(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork("BG_SYNCP") + WorkManager.getInstance(context).cancelUniqueWork(BG_SYNC_WORK_NAME) } } } diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml index 1a35caff41..51234e0bba 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector/src/fdroid/AndroidManifest.xml @@ -20,10 +20,6 @@ android:enabled="true" android:exported="false" /> - - \ No newline at end of file diff --git a/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt index 6648d79490..951fcaa14d 100644 --- a/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt +++ b/vector/src/fdroid/java/im/vector/riotx/fdroid/receiver/AlarmSyncBroadcastReceiver.kt @@ -25,7 +25,7 @@ import android.os.Build import android.os.PowerManager import androidx.core.content.ContextCompat import im.vector.matrix.android.internal.session.sync.job.SyncService -import im.vector.riotx.fdroid.service.VectorSyncService +import im.vector.riotx.core.services.VectorSyncService import timber.log.Timber class AlarmSyncBroadcastReceiver : BroadcastReceiver() { @@ -41,14 +41,9 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { val userId = intent.getStringExtra(SyncService.EXTRA_USER_ID) // This method is called when the BroadcastReceiver is receiving an Intent broadcast. Timber.d("RestartBroadcastReceiver received intent") - Intent(context, VectorSyncService::class.java).also { - it.putExtra(SyncService.EXTRA_USER_ID, userId) + VectorSyncService.newIntent(context, userId).also { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ContextCompat.startForegroundService(context, it) - } else { - context.startService(it) - } + ContextCompat.startForegroundService(context, it) } catch (ex: Throwable) { // TODO Timber.e(ex) @@ -79,6 +74,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { } fun cancelAlarm(context: Context) { + Timber.v("Cancel alarm") val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java) val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT) val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 5f1687c9c9..49af440501 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -104,6 +104,10 @@ android:name=".core.services.CallService" android:exported="false" /> + + diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index b6b0d16360..3f55e8f5e6 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -116,11 +116,12 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! activeSessionHolder.setActiveSession(lastAuthenticatedSession) - lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener, sessionListener) + lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener) } ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun entersForeground() { + Timber.i("App entered foreground") FcmHelper.onEnterForeground(appContext) activeSessionHolder.getSafeActiveSession()?.also { it.stopAnyBackgroundSync() diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt index 0ce4d04497..9a6905e4c7 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt @@ -16,23 +16,27 @@ package im.vector.riotx.core.extensions +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.sync.FilterService +import im.vector.matrix.android.internal.session.sync.job.SyncService +import im.vector.riotx.core.services.VectorSyncService import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import timber.log.Timber -fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener, +fun Session.configureAndStart(context: Context, + pushRuleTriggerListener: PushRuleTriggerListener, sessionListener: SessionListener) { open() addListener(sessionListener) setFilter(FilterService.FilterPreset.RiotFilter) Timber.i("Configure and start session for ${this.myUserId}") - val isAtLeastStarted = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) - Timber.v("--> is at least started? $isAtLeastStarted") - startSync(isAtLeastStarted) + startSyncing(context) refreshPushers() pushRuleTriggerListener.startWithSession(this) @@ -40,3 +44,20 @@ fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener, // @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler // @Inject lateinit var keyRequestHandler: KeyRequestHandler } + +fun Session.startSyncing(context: Context) { + val applicationContext = context.applicationContext + if (!hasAlreadySynced()) { + VectorSyncService.newIntent(applicationContext, myUserId).also { + try { + ContextCompat.startForegroundService(applicationContext, it) + } catch (ex: Throwable) { + // TODO + Timber.e(ex) + } + } + } + val isAtLeastStarted = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) + Timber.v("--> is at least started? $isAtLeastStarted") + startSync(isAtLeastStarted) +} diff --git a/vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt similarity index 83% rename from vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt rename to vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt index fbf9c1a031..175f921e07 100644 --- a/vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt +++ b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotx.fdroid.service +package im.vector.riotx.core.services import android.app.NotificationManager import android.content.Context import android.content.Intent import android.os.Build +import androidx.core.content.ContextCompat import im.vector.matrix.android.internal.session.sync.job.SyncService import im.vector.riotx.R import im.vector.riotx.core.extensions.vectorComponent @@ -27,6 +28,15 @@ import timber.log.Timber class VectorSyncService : SyncService() { + companion object { + + fun newIntent(context: Context, userId: String): Intent { + return Intent(context, VectorSyncService::class.java).also { + it.putExtra(EXTRA_USER_ID, userId) + } + } + } + private lateinit var notificationUtils: NotificationUtils override fun onCreate() { @@ -45,8 +55,7 @@ class VectorSyncService : SyncService() { } /** - * Service is started only in fdroid mode when no FCM is available - * Otherwise it is bounded + * Service is started in fdroid mode when no FCM is available or is used for initialSync */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Timber.v("VectorSyncService - onStartCommand ") diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index 7064ad0d49..b95eddd011 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -26,6 +26,8 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.configureAndStart +import im.vector.riotx.core.extensions.startSyncing import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.deleteAllFiles import im.vector.riotx.features.home.HomeActivity @@ -75,8 +77,13 @@ class MainActivity : VectorBaseActivity() { } private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) { + val session = sessionHolder.getSafeActiveSession() + if (session == null) { + start() + return + } when { - clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback { + clearCredentials -> session.signOut(object : MatrixCallback { override fun onSuccess(data: Unit) { Timber.w("SIGN_OUT: success, start app") sessionHolder.clearActiveSession() @@ -87,8 +94,9 @@ class MainActivity : VectorBaseActivity() { displayError(failure, clearCache, clearCredentials) } }) - clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback { + clearCache -> session.clearCache(object : MatrixCallback { override fun onSuccess(data: Unit) { + session.startSyncing(applicationContext) doLocalCleanupAndStart() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 00207cbfbf..11f2f149a8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.login +import android.content.Context import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -44,6 +45,7 @@ import java.util.concurrent.CancellationException * */ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, + private val applicationContext: Context, private val authenticationService: AuthenticationService, private val activeSessionHolder: ActiveSessionHolder, private val pushRuleTriggerListener: PushRuleTriggerListener, @@ -469,7 +471,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun onSessionCreated(session: Session) { activeSessionHolder.setActiveSession(session) - session.configureAndStart(pushRuleTriggerListener, sessionListener) + session.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener) setState { copy( asyncLoginAction = Success(Unit) From 3a269be2ef253e6f1ba9611baf657729a82617d1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Dec 2019 16:24:30 +0100 Subject: [PATCH 002/105] Sync: fix crash on gplay flavor and reschedule when no network instead of showing a potential notification all the time --- .../network/NetworkConnectivityChecker.kt | 3 ++ .../internal/session/sync/job/SyncService.kt | 19 +++++---- vector/src/gplay/AndroidManifest.xml | 2 + .../riotx/core/services/VectorSyncService.kt | 42 ++++++++++++++----- 4 files changed, 48 insertions(+), 18 deletions(-) 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 4d4699810f..e6dda3f5e1 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 @@ -62,6 +62,9 @@ internal class NetworkConnectivityChecker @Inject constructor(private val contex override fun onMoveToForeground() { merlin.bind() + merlinsBeard.hasInternetAccess { + hasInternetAccess = it + } merlin.registerDisconnectable { if (hasInternetAccess) { Timber.v("On Disconnect") 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 3aeb408db1..8a91186e07 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 @@ -34,8 +34,9 @@ import java.util.concurrent.atomic.AtomicBoolean * in order to be able to perform a sync even if the app is not running. * The and must be declared in the Manifest or the app using the SDK */ -open class SyncService : Service() { +abstract class SyncService : Service() { + private var userId: String? = null private var mIsSelfDestroyed: Boolean = false private lateinit var syncTask: SyncTask @@ -50,9 +51,10 @@ open class SyncService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Timber.i("onStartCommand $intent") intent?.let { - val userId = it.getStringExtra(EXTRA_USER_ID) - val sessionComponent = Matrix.getInstance(applicationContext).sessionManager.getSessionComponent(userId) + val safeUserId = it.getStringExtra(EXTRA_USER_ID) ?: return@let + val sessionComponent = Matrix.getInstance(applicationContext).sessionManager.getSessionComponent(safeUserId) ?: return@let + userId = safeUserId syncTask = sessionComponent.syncTask() networkConnectivityChecker = sessionComponent.networkConnectivityChecker() taskExecutor = sessionComponent.taskExecutor() @@ -87,9 +89,11 @@ open class SyncService : Service() { private suspend fun doSync() { if (!networkConnectivityChecker.hasInternetAccess()) { - Timber.v("No network, try to sync again in 10s") - delay(DELAY_NO_NETWORK) - doSync() + Timber.v("No network reschedule to avoid wasting resources") + userId?.also { + onRescheduleAsked(it, delay = 10_000L) + } + stopMe() return } Timber.v("Execute sync request with timeout 0") @@ -109,6 +113,8 @@ open class SyncService : Service() { } } + abstract fun onRescheduleAsked(userId: String, delay: Long) + override fun onBind(intent: Intent?): IBinder? { return null } @@ -116,7 +122,6 @@ open class SyncService : Service() { companion object { const val EXTRA_USER_ID = "EXTRA_USER_ID" private const val TIME_OUT = 0L - private const val DELAY_NO_NETWORK = 10_000L private const val DELAY_FAILURE = 5_000L } } diff --git a/vector/src/gplay/AndroidManifest.xml b/vector/src/gplay/AndroidManifest.xml index db30f21f7a..85567df2c1 100755 --- a/vector/src/gplay/AndroidManifest.xml +++ b/vector/src/gplay/AndroidManifest.xml @@ -2,6 +2,8 @@ + + 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 175f921e07..93c08c50d6 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,11 +15,12 @@ */ 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 androidx.core.content.ContextCompat import im.vector.matrix.android.internal.session.sync.job.SyncService import im.vector.riotx.R import im.vector.riotx.core.extensions.vectorComponent @@ -44,16 +45,6 @@ class VectorSyncService : SyncService() { notificationUtils = vectorComponent().notificationUtils() } - override fun onDestroy() { - removeForegroundNotif() - super.onDestroy() - } - - private fun removeForegroundNotif() { - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE) - } - /** * Service is started in fdroid mode when no FCM is available or is used for initialSync */ @@ -65,4 +56,33 @@ class VectorSyncService : SyncService() { } return super.onStartCommand(intent, flags, startId) } + + override fun onDestroy() { + removeForegroundNotif() + super.onDestroy() + } + + override fun onRescheduleAsked(userId: String, delay: Long) { + reschedule(userId, delay) + } + + private fun removeForegroundNotif() { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE) + } + + private fun reschedule(userId: String, delay: Long) { + val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PendingIntent.getForegroundService(this, 0, newIntent(this, userId), 0) + } else { + PendingIntent.getService(this, 0, newIntent(this, userId), 0) + } + val firstMillis = System.currentTimeMillis() + delay + val alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) + } else { + alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) + } + } } From 6b61c958432254c752d1bd7e18f3390899861b19 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Dec 2019 20:39:07 +0100 Subject: [PATCH 003/105] Coroutines: introduce a sequencer --- .../internal/session/sync/job/SyncThread.kt | 83 +++++++---------- .../internal/task/CoroutineSequencer.kt | 93 +++++++++++++++++++ .../task/MatrixCoroutineSequencersTest.kt | 61 ++++++++++++ 3 files changed, 190 insertions(+), 47 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt create mode 100644 matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt 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 67169cdc3c..01923b103f 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 @@ -30,6 +30,8 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.BackgroundDetectionObserver +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import timber.log.Timber import java.net.SocketTimeoutException import java.util.concurrent.CountDownLatch @@ -99,9 +101,9 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, isStarted = true networkConnectivityChecker.register(this) backgroundDetectionObserver.register(this) + while (state != SyncState.KILLING) { Timber.v("Entering loop, state: $state") - if (!networkConnectivityChecker.hasInternetAccess()) { Timber.v("No network. Waiting...") updateStateTo(SyncState.NO_NETWORK) @@ -116,57 +118,13 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (state !is SyncState.RUNNING) { updateStateTo(SyncState.RUNNING(afterPause = true)) } - // No timeout after a pause val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } - Timber.v("Execute sync request with timeout $timeout") - val latch = CountDownLatch(1) val params = SyncTask.Params(timeout) - - cancelableTask = syncTask.configureWith(params) { - this.callbackThread = TaskThread.SYNC - this.executionThread = TaskThread.SYNC - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("onSuccess") - latch.countDown() - } - - override fun onFailure(failure: Throwable) { - if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) { - // Timeout are not critical - Timber.v("Timeout") - } else if (failure is Failure.Cancelled) { - Timber.v("Cancelled") - } else if (failure is Failure.ServerError - && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { - // No token or invalid token, stop the thread - Timber.w(failure) - updateStateTo(SyncState.KILLING) - } else { - Timber.e(failure) - - if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) { - // Wait 10s before retrying - Timber.v("Wait 10s") - sleep(RETRY_WAIT_TIME_MS) - } - } - - latch.countDown() - } - } + runBlocking { + doSync(params) } - .executeBy(taskExecutor) - - latch.await() - state.let { - if (it is SyncState.RUNNING && it.afterPause) { - updateStateTo(SyncState.RUNNING(afterPause = false)) - } - } - Timber.v("...Continue") } } @@ -176,6 +134,37 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, networkConnectivityChecker.unregister(this) } + private suspend fun doSync(params: SyncTask.Params) { + try { + syncTask.execute(params) + } catch (failure: Throwable) { + if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) { + // Timeout are not critical + Timber.v("Timeout") + } else if (failure is Failure.Cancelled) { + Timber.v("Cancelled") + } else if (failure is Failure.ServerError + && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { + // No token or invalid token, stop the thread + Timber.w(failure) + updateStateTo(SyncState.KILLING) + } else { + Timber.e(failure) + if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) { + // Wait 10s before retrying + Timber.v("Wait 10s") + delay(RETRY_WAIT_TIME_MS) + } + } + } finally { + state.let { + if (it is SyncState.RUNNING && it.afterPause) { + updateStateTo(SyncState.RUNNING(afterPause = false)) + } + } + } + } + private fun updateStateTo(newState: SyncState) { Timber.v("Update state from $state to $newState") state = newState diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt new file mode 100644 index 0000000000..c0802f9fae --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.task + +import im.vector.matrix.android.internal.di.MatrixScope +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import java.util.concurrent.Executors +import javax.inject.Inject + + +@MatrixScope +internal class MatrixCoroutineSequencers @Inject constructor() { + + private val sequencers = HashMap() + + suspend fun post(name: String, block: suspend CoroutineScope.() -> Any): Any { + val sequencer = sequencers.getOrPut(name) { + ChannelCoroutineSequencer() + } + return sequencer.post(block) + } + + fun cancel(name: String) { + sequencers.remove(name)?.cancel() + } + + fun cancelAll() { + sequencers.values.forEach { + it.cancel() + } + sequencers.clear() + } + +} + +internal interface CoroutineSequencer { + suspend fun post(block: suspend CoroutineScope.() -> Any): Any + fun cancel() +} + +internal class ChannelCoroutineSequencer : CoroutineSequencer { + + private data class Message( + val block: suspend CoroutineScope.() -> Any, + val deferred: CompletableDeferred + ) + + private val messageChannel: Channel = Channel() + private val coroutineScope = CoroutineScope(SupervisorJob()) + private val singleDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + init { + coroutineScope.launch(singleDispatcher) { + for (message in messageChannel) { + try { + val result = message.block(this) + message.deferred.complete(result) + } catch (exception: Throwable) { + message.deferred.completeExceptionally(exception) + } + } + } + } + + override fun cancel() { + messageChannel.cancel() + coroutineScope.coroutineContext.cancelChildren() + } + + override suspend fun post(block: suspend CoroutineScope.() -> Any): Any { + val deferred = CompletableDeferred() + val message = Message(block, deferred) + messageChannel.send(message) + return deferred.await() + } + +} + diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt new file mode 100644 index 0000000000..10cfd92fc5 --- /dev/null +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.task + +import kotlinx.coroutines.* +import org.junit.Test +import java.util.concurrent.Executors + +class MatrixCoroutineSequencersTest { + + @Test + fun sequencer_should_run_sequential() { + val sequencer = MatrixCoroutineSequencers() + val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + val jobs = listOf( + GlobalScope.launch(dispatcher) { + sequencer.post("Sequencer1") { suspendingMethod("#3") } + }, + GlobalScope.launch(dispatcher) { + sequencer.post("Sequencer1") { suspendingMethod("#4") } + }, + GlobalScope.launch(dispatcher) { + sequencer.post("Sequencer2") { suspendingMethod("#5") } + }, + GlobalScope.launch(dispatcher) { + sequencer.post("Sequencer2") { suspendingMethod("#6") } + }, + GlobalScope.launch(dispatcher) { + sequencer.post("Sequencer2") { suspendingMethod("#7") } + } + ) + Thread.sleep(5500) + sequencer.cancelAll() + runBlocking { + jobs.joinAll() + } + } + + private suspend fun suspendingMethod(name: String): String = withContext(Dispatchers.Default) { + println("BLOCKING METHOD $name STARTS") + delay(3000) + println("BLOCKING METHOD $name ENDS") + name + } + +} From eab94b4f03cc220a00a91907432db8eefad2cb7a Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 12 Dec 2019 14:30:40 +0100 Subject: [PATCH 004/105] Sequencer: handle cancellation --- .../internal/task/CoroutineSequencer.kt | 77 ++++++------- .../task/MatrixCoroutineSequencersTest.kt | 108 +++++++++++++++--- 2 files changed, 125 insertions(+), 60 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt index c0802f9fae..0f8439b144 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt @@ -16,59 +16,43 @@ package im.vector.matrix.android.internal.task -import im.vector.matrix.android.internal.di.MatrixScope -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.intrinsics.startCoroutineCancellable +import kotlinx.coroutines.launch import java.util.concurrent.Executors -import javax.inject.Inject - -@MatrixScope -internal class MatrixCoroutineSequencers @Inject constructor() { - - private val sequencers = HashMap() - - suspend fun post(name: String, block: suspend CoroutineScope.() -> Any): Any { - val sequencer = sequencers.getOrPut(name) { - ChannelCoroutineSequencer() - } - return sequencer.post(block) - } - - fun cancel(name: String) { - sequencers.remove(name)?.cancel() - } - - fun cancelAll() { - sequencers.values.forEach { - it.cancel() - } - sequencers.clear() - } - -} - -internal interface CoroutineSequencer { - suspend fun post(block: suspend CoroutineScope.() -> Any): Any +internal interface CoroutineSequencer { + suspend fun post(block: suspend () -> T): T fun cancel() + fun close() } -internal class ChannelCoroutineSequencer : CoroutineSequencer { +internal class ChannelCoroutineSequencer : CoroutineSequencer { - private data class Message( - val block: suspend CoroutineScope.() -> Any, - val deferred: CompletableDeferred + private data class Message( + val block: suspend () -> T, + val deferred: CompletableDeferred ) - private val messageChannel: Channel = Channel() + private val messageChannel: Channel> = Channel() private val coroutineScope = CoroutineScope(SupervisorJob()) private val singleDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() init { + launchCoroutine() + } + + private fun launchCoroutine() { coroutineScope.launch(singleDispatcher) { for (message in messageChannel) { try { - val result = message.block(this) + val result = message.block() message.deferred.complete(result) } catch (exception: Throwable) { message.deferred.completeExceptionally(exception) @@ -77,16 +61,27 @@ internal class ChannelCoroutineSequencer : CoroutineSequencer { } } - override fun cancel() { + override fun close() { messageChannel.cancel() coroutineScope.coroutineContext.cancelChildren() } - override suspend fun post(block: suspend CoroutineScope.() -> Any): Any { - val deferred = CompletableDeferred() + override fun cancel() { + close() + launchCoroutine() + } + + override suspend fun post(block: suspend () -> T): T { + val deferred = CompletableDeferred() val message = Message(block, deferred) messageChannel.send(message) - return deferred.await() + return try { + deferred.await() + } catch (cancellation: CancellationException) { + coroutineScope.coroutineContext.cancelChildren() + launchCoroutine() + throw cancellation + } } } diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt index 10cfd92fc5..c69146c3dc 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt @@ -16,46 +16,116 @@ package im.vector.matrix.android.internal.task -import kotlinx.coroutines.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals import org.junit.Test import java.util.concurrent.Executors class MatrixCoroutineSequencersTest { + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + @Test fun sequencer_should_run_sequential() { - val sequencer = MatrixCoroutineSequencers() - val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + val sequencer = ChannelCoroutineSequencer() + val results = ArrayList() val jobs = listOf( GlobalScope.launch(dispatcher) { - sequencer.post("Sequencer1") { suspendingMethod("#3") } + sequencer.post { suspendingMethod("#1") }.also { + results.add(it) + } }, GlobalScope.launch(dispatcher) { - sequencer.post("Sequencer1") { suspendingMethod("#4") } + sequencer.post { suspendingMethod("#2") }.also { + results.add(it) + } }, GlobalScope.launch(dispatcher) { - sequencer.post("Sequencer2") { suspendingMethod("#5") } - }, - GlobalScope.launch(dispatcher) { - sequencer.post("Sequencer2") { suspendingMethod("#6") } - }, - GlobalScope.launch(dispatcher) { - sequencer.post("Sequencer2") { suspendingMethod("#7") } + sequencer.post { suspendingMethod("#3") }.also { + results.add(it) + } } ) - Thread.sleep(5500) - sequencer.cancelAll() runBlocking { jobs.joinAll() } + assertEquals(3, results.size) + assertEquals(results[0], "#1") + assertEquals(results[1], "#2") + assertEquals(results[2], "#3") } - private suspend fun suspendingMethod(name: String): String = withContext(Dispatchers.Default) { - println("BLOCKING METHOD $name STARTS") - delay(3000) - println("BLOCKING METHOD $name ENDS") - name + @Test + fun sequencer_should_run_parallel() { + val sequencer1 = ChannelCoroutineSequencer() + val sequencer2 = ChannelCoroutineSequencer() + val sequencer3 = ChannelCoroutineSequencer() + val results = ArrayList() + val jobs = listOf( + GlobalScope.launch(dispatcher) { + sequencer1.post { suspendingMethod("#1") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer2.post { suspendingMethod("#2") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer3.post { suspendingMethod("#3") }.also { + results.add(it) + } + } + ) + runBlocking { + jobs.joinAll() + } + assertEquals(3, results.size) + } + + @Test + fun sequencer_should_jump_to_next_when_current_job_canceled() { + val sequencer = ChannelCoroutineSequencer() + val results = ArrayList() + val jobs = listOf( + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#1") }.also { + results.add(it) + } + + }, + GlobalScope.launch(dispatcher) { + val result = sequencer.post { suspendingMethod("#2") }.also { + results.add(it) + } + println("Result: $result") + }, + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#3") }.also { + results.add(it) + } + } + ) + // We are canceling the second job + jobs[1].cancel() + runBlocking { + jobs.joinAll() + } + assertEquals(2, results.size) + } + + private suspend fun suspendingMethod(name: String): String { + println("BLOCKING METHOD $name STARTS on ${Thread.currentThread().name}") + delay(1000) + println("BLOCKING METHOD $name ENDS on ${Thread.currentThread().name}") + return name } } From fe2be90002519986e29956380f69e4a4d2fcd19b Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 13 Dec 2019 15:30:57 +0100 Subject: [PATCH 005/105] Sync: use the CoroutineSequencer but need more tests --- .../android/internal/di/MatrixModule.kt | 3 +- .../internal/session/DefaultSession.kt | 3 ++ .../android/internal/session/sync/SyncTask.kt | 9 +++-- .../session/sync/SyncTaskSequencer.kt | 24 +++++++++++++ .../internal/session/sync/job/SyncService.kt | 2 +- .../internal/session/sync/job/SyncThread.kt | 20 +++++------ .../internal/session/sync/job/SyncWorker.kt | 8 +---- .../internal/task/CoroutineSequencer.kt | 35 ++++++++++--------- .../android/internal/task/TaskExecutor.kt | 1 - .../android/internal/task/TaskThread.kt | 3 +- .../util/MatrixCoroutineDispatchers.kt | 3 +- ...cersTest.kt => CoroutineSequencersTest.kt} | 2 +- 12 files changed, 68 insertions(+), 45 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt rename matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/{MatrixCoroutineSequencersTest.kt => CoroutineSequencersTest.kt} (99%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index c17864b82b..8b37d01b70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -38,8 +38,7 @@ internal object MatrixModule { return MatrixCoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default, main = Dispatchers.Main, - crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), - sync = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher() ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 32126fb78d..ac86d437ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -44,6 +44,7 @@ import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.database.LiveEntityObserver +import im.vector.matrix.android.internal.session.sync.SyncTaskSequencer import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncWorker @@ -74,6 +75,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, private val syncTokenStore: SyncTokenStore, + private val syncTaskSequencer: SyncTaskSequencer, private val contentUploadProgressTracker: ContentUploadStateTracker, private val initialSyncProgressService: Lazy, private val homeServerCapabilitiesService: Lazy) @@ -143,6 +145,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se cryptoService.get().close() isOpen = false EventBus.getDefault().unregister(this) + syncTaskSequencer.close() } override fun syncState(): LiveData { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 1082fa835b..688f159fb0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -45,10 +45,15 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, private val initialSyncProgressService: DefaultInitialSyncProgressService, private val syncTokenStore: SyncTokenStore, private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, - private val userStore: UserStore + private val userStore: UserStore, + private val syncTaskSequencer: SyncTaskSequencer ) : SyncTask { - override suspend fun execute(params: SyncTask.Params) { + override suspend fun execute(params: SyncTask.Params) = syncTaskSequencer.post { + doSync(params) + } + + private suspend fun doSync(params: SyncTask.Params) { Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") // Maybe refresh the home server capabilities data we know getHomeServerCapabilitiesTask.execute(Unit) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt new file mode 100644 index 0000000000..bfa49b7af5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTaskSequencer.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.sync + +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.task.ChannelCoroutineSequencer +import javax.inject.Inject + +@SessionScope +internal class SyncTaskSequencer @Inject constructor() : ChannelCoroutineSequencer() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt index 8a91186e07..ce46531045 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 @@ -63,7 +63,7 @@ abstract class SyncService : Service() { Timber.i("Received a start while was already syncing... ignore") } else { isRunning.set(true) - serviceScope.launch(coroutineDispatchers.sync) { + serviceScope.launch(coroutineDispatchers.io) { doSync() } } 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 01923b103f..1acb7dbb4a 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 @@ -30,8 +30,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.BackgroundDetectionObserver -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import timber.log.Timber import java.net.SocketTimeoutException import java.util.concurrent.CountDownLatch @@ -42,14 +41,13 @@ private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private val networkConnectivityChecker: NetworkConnectivityChecker, - private val backgroundDetectionObserver: BackgroundDetectionObserver, - private val taskExecutor: TaskExecutor -) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { + private val backgroundDetectionObserver: BackgroundDetectionObserver) + : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.IDLE private var liveState = MutableLiveData() private val lock = Object() - private var cancelableTask: Cancelable? = null + private val syncScope = CoroutineScope(SupervisorJob()) private var isStarted = false @@ -74,14 +72,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (isStarted) { Timber.v("Pause sync...") isStarted = false - cancelableTask?.cancel() + syncScope.coroutineContext.cancelChildren() } } fun kill() = synchronized(lock) { Timber.v("Kill sync...") updateStateTo(SyncState.KILLING) - cancelableTask?.cancel() + syncScope.coroutineContext.cancelChildren() lock.notify() } @@ -101,7 +99,6 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, isStarted = true networkConnectivityChecker.register(this) backgroundDetectionObserver.register(this) - while (state != SyncState.KILLING) { Timber.v("Entering loop, state: $state") if (!networkConnectivityChecker.hasInternetAccess()) { @@ -122,9 +119,12 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } Timber.v("Execute sync request with timeout $timeout") val params = SyncTask.Params(timeout) - runBlocking { + val sync = syncScope.launch { doSync(params) } + runBlocking { + sync.join() + } Timber.v("...Continue") } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt index 804102913d..eb4f2ff7c2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt @@ -18,19 +18,14 @@ package im.vector.matrix.android.internal.session.sync.job import android.content.Context import androidx.work.* import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.isTokenError import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -50,7 +45,6 @@ internal class SyncWorker(context: Context, @Inject lateinit var syncTask: SyncTask @Inject lateinit var taskExecutor: TaskExecutor - @Inject lateinit var coroutineDispatchers: MatrixCoroutineDispatchers @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker override suspend fun doWork(): Result { @@ -72,7 +66,7 @@ internal class SyncWorker(context: Context, ) } - private suspend fun doSync(timeout: Long) = withContext(coroutineDispatchers.sync) { + private suspend fun doSync(timeout: Long) { val taskParams = SyncTask.Params(timeout) syncTask.execute(taskParams) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt index 0f8439b144..1df0dae901 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt @@ -16,32 +16,36 @@ package im.vector.matrix.android.internal.task -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.intrinsics.startCoroutineCancellable -import kotlinx.coroutines.launch import java.util.concurrent.Executors +/** + * This class intends to be used for ensure suspendable methods are played sequentially all the way long. + */ internal interface CoroutineSequencer { + /** + * @param block the suspendable block to execute + * @return the result of the block + */ suspend fun post(block: suspend () -> T): T - fun cancel() + + /** + * Cancel all and close, so you won't be able to post anything else after + */ fun close() } -internal class ChannelCoroutineSequencer : CoroutineSequencer { +internal open class ChannelCoroutineSequencer : CoroutineSequencer { private data class Message( val block: suspend () -> T, val deferred: CompletableDeferred ) - private val messageChannel: Channel> = Channel() + private var messageChannel: Channel> = Channel() private val coroutineScope = CoroutineScope(SupervisorJob()) + // This will ensure private val singleDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() init { @@ -62,13 +66,8 @@ internal class ChannelCoroutineSequencer : CoroutineSequencer { } override fun close() { - messageChannel.cancel() coroutineScope.coroutineContext.cancelChildren() - } - - override fun cancel() { - close() - launchCoroutine() + messageChannel.close() } override suspend fun post(block: suspend () -> T): T { @@ -78,6 +77,8 @@ internal class ChannelCoroutineSequencer : CoroutineSequencer { return try { deferred.await() } catch (cancellation: CancellationException) { + // In case of cancellation, we stop the current coroutine context + // and relaunch one to consume next messages coroutineScope.coroutineContext.cancelChildren() launchCoroutine() throw cancellation diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt index d5392779d1..244cc83901 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt @@ -85,6 +85,5 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers TaskThread.IO -> coroutineDispatchers.io TaskThread.CALLER -> EmptyCoroutineContext TaskThread.CRYPTO -> coroutineDispatchers.crypto - TaskThread.SYNC -> coroutineDispatchers.sync } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt index 16ed93662c..c04e9fbce6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt @@ -21,6 +21,5 @@ internal enum class TaskThread { COMPUTATION, IO, CALLER, - CRYPTO, - SYNC + CRYPTO } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt index 23201c084e..d15389f703 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt @@ -22,6 +22,5 @@ internal data class MatrixCoroutineDispatchers( val io: CoroutineDispatcher, val computation: CoroutineDispatcher, val main: CoroutineDispatcher, - val crypto: CoroutineDispatcher, - val sync: CoroutineDispatcher + val crypto: CoroutineDispatcher ) diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt similarity index 99% rename from matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt rename to matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt index c69146c3dc..a367632d9f 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/MatrixCoroutineSequencersTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt @@ -26,7 +26,7 @@ import org.junit.Assert.assertEquals import org.junit.Test import java.util.concurrent.Executors -class MatrixCoroutineSequencersTest { +class CoroutineSequencersTest { private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() From 5dd46e82d75b5816df11c93883cfbb78acd40a5f Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 13 Dec 2019 18:21:44 +0100 Subject: [PATCH 006/105] Sync: make only one big transaction to avoid having bad states --- .../android/internal/di/MatrixModule.kt | 1 - .../internal/session/sync/GroupSyncHandler.kt | 18 +- .../internal/session/sync/RoomSyncHandler.kt | 48 ++--- .../session/sync/SyncResponseHandler.kt | 72 +++++-- .../android/internal/session/sync/SyncTask.kt | 2 - .../internal/session/sync/SyncTokenStore.kt | 9 +- .../sync/UserAccountDataSyncHandler.kt | 193 +++++++++++------- .../internal/session/sync/job/SyncThread.kt | 6 - .../internal/task/CoroutineSequencer.kt | 2 - .../internal/task/CoroutineSequencersTest.kt | 2 - .../vector/riotx/core/extensions/Session.kt | 2 - .../im/vector/riotx/features/MainActivity.kt | 1 - 12 files changed, 196 insertions(+), 160 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index 8b37d01b70..04b8565546 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.olm.OlmManager -import java.util.concurrent.Executors @Module internal object MatrixModule { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt index 2ca9b6cccc..392db0a73c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.sync -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.model.GroupEntity @@ -25,11 +24,10 @@ import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressServi import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.sync.model.GroupsSyncResponse import im.vector.matrix.android.internal.session.sync.model.InvitedGroupSync -import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import javax.inject.Inject -internal class GroupSyncHandler @Inject constructor(private val monarchy: Monarchy) { +internal class GroupSyncHandler @Inject constructor() { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -37,12 +35,14 @@ internal class GroupSyncHandler @Inject constructor(private val monarchy: Monarc data class LEFT(val data: Map) : HandlingStrategy() } - suspend fun handle(roomsSyncResponse: GroupsSyncResponse, reporter: DefaultInitialSyncProgressService? = null) { - monarchy.awaitTransaction { realm -> - handleGroupSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter) - handleGroupSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter) - handleGroupSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter) - } + fun handle( + realm: Realm, + roomsSyncResponse: GroupsSyncResponse, + reporter: DefaultInitialSyncProgressService? = null + ) { + handleGroupSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter) + handleGroupSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter) + handleGroupSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter) } // PRIVATE METHODS ***************************************************************************** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 4a003eb7d9..a080a5158e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.session.sync -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R -import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel @@ -34,31 +32,21 @@ import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoo import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.mapWithProgress -import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService -import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.sync.model.* import im.vector.matrix.android.internal.session.user.UserEntityFactory -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import io.realm.kotlin.createObject import timber.log.Timber import javax.inject.Inject -internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarchy, - private val readReceiptHandler: ReadReceiptHandler, +internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler, private val roomSummaryUpdater: RoomSummaryUpdater, private val roomTagHandler: RoomTagHandler, private val roomFullyReadHandler: RoomFullyReadHandler, - private val cryptoService: DefaultCryptoService, - private val tokenStore: SyncTokenStore, - private val pushRuleService: DefaultPushRuleService, - private val processForPushTask: ProcessEventForPushTask, - private val taskExecutor: TaskExecutor) { + private val cryptoService: DefaultCryptoService) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -66,28 +54,16 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch data class LEFT(val data: Map) : HandlingStrategy() } - suspend fun handle(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService? = null) { + fun handle( + realm: Realm, + roomsSyncResponse: RoomsSyncResponse, + isInitialSync: Boolean, + reporter: DefaultInitialSyncProgressService? = null + ) { Timber.v("Execute transaction from $this") - monarchy.awaitTransaction { realm -> - handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter) - handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter) - handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter) - } - // handle event for bing rule checks - checkPushRules(roomsSyncResponse) - } - - private fun checkPushRules(roomsSyncResponse: RoomsSyncResponse) { - Timber.v("[PushRules] --> checkPushRules") - if (tokenStore.getLastToken() == null) { - Timber.v("[PushRules] <-- No push rule check on initial sync") - return - } // nothing on initial sync - - val rules = pushRuleService.getPushRules(RuleScope.GLOBAL) - processForPushTask.configureWith(ProcessEventForPushTask.Params(roomsSyncResponse, rules)) - .executeBy(taskExecutor) - Timber.v("[PushRules] <-- Push task scheduled") + handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter) } // PRIVATE METHODS ***************************************************************************** @@ -137,7 +113,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch if (roomSync.state != null && roomSync.state.events.isNotEmpty()) { val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt() - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val untimelinedStateIndex = minStateIndex + 1 roomSync.state.events.forEach { event -> roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt index 1ae185b073..1454fdae7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt @@ -16,20 +16,30 @@ package im.vector.matrix.android.internal.session.sync +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R +import im.vector.matrix.android.api.pushrules.PushRuleService +import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService +import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask import im.vector.matrix.android.internal.session.reportSubtask +import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse import im.vector.matrix.android.internal.session.sync.model.SyncResponse +import im.vector.matrix.android.internal.util.awaitTransaction import timber.log.Timber import javax.inject.Inject import kotlin.system.measureTimeMillis -internal class SyncResponseHandler @Inject constructor(private val roomSyncHandler: RoomSyncHandler, +internal class SyncResponseHandler @Inject constructor(private val monarchy: Monarchy, + private val roomSyncHandler: RoomSyncHandler, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, private val groupSyncHandler: GroupSyncHandler, private val cryptoSyncHandler: CryptoSyncHandler, private val cryptoService: DefaultCryptoService, + private val tokenStore: SyncTokenStore, + private val processEventForPushTask: ProcessEventForPushTask, + private val pushRuleService: PushRuleService, private val initialSyncProgressService: DefaultInitialSyncProgressService) { suspend fun handleResponse(syncResponse: SyncResponse, fromToken: String?) { @@ -45,26 +55,27 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl }.also { Timber.v("Finish handling start cryptoService in $it ms") } - val measure = measureTimeMillis { - // Handle the to device events before the room ones - // to ensure to decrypt them properly - measureTimeMillis { - Timber.v("Handle toDevice") - reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) { - if (syncResponse.toDevice != null) { - cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) - } - } - }.also { - Timber.v("Finish handling toDevice in $it ms") - } + // Handle the to device events before the room ones + // to ensure to decrypt them properly + measureTimeMillis { + Timber.v("Handle toDevice") + reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) { + if (syncResponse.toDevice != null) { + cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + } + } + }.also { + Timber.v("Finish handling toDevice in $it ms") + } + + // Start one big transaction + monarchy.awaitTransaction { realm -> measureTimeMillis { Timber.v("Handle rooms") - reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) { if (syncResponse.rooms != null) { - roomSyncHandler.handle(syncResponse.rooms, isInitialSync, reporter) + roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, reporter) } } }.also { @@ -75,7 +86,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl reportSubtask(reporter, R.string.initial_sync_start_importing_account_groups, 100, 0.1f) { Timber.v("Handle groups") if (syncResponse.groups != null) { - groupSyncHandler.handle(syncResponse.groups, reporter) + groupSyncHandler.handle(realm, syncResponse.groups, reporter) } } }.also { @@ -85,15 +96,32 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl measureTimeMillis { reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) { Timber.v("Handle accountData") - userAccountDataSyncHandler.handle(syncResponse.accountData, syncResponse.rooms?.invite) + userAccountDataSyncHandler.handle(realm, syncResponse.accountData) } }.also { Timber.v("Finish handling accountData in $it ms") } - - Timber.v("On sync completed") - cryptoSyncHandler.onSyncCompleted(syncResponse) + tokenStore.saveToken(realm, syncResponse.nextBatch) } - Timber.v("Finish handling sync in $measure ms") + + // Everything else we need to do outside the transaction + syncResponse.rooms?.also { + checkPushRules(it, isInitialSync) + userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) + } + Timber.v("On sync completed") + cryptoSyncHandler.onSyncCompleted(syncResponse) + } + + private suspend fun checkPushRules(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean) { + Timber.v("[PushRules] --> checkPushRules") + if (isInitialSync) { + Timber.v("[PushRules] <-- No push rule check on initial sync") + return + } // nothing on initial sync + + val rules = pushRuleService.getPushRules(RuleScope.GLOBAL) + processEventForPushTask.execute(ProcessEventForPushTask.Params(roomsSyncResponse, rules)) + Timber.v("[PushRules] <-- Push task scheduled") } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 688f159fb0..fbc419d3b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -21,7 +21,6 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.internal.auth.SessionParamsStore 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.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.filter.FilterRepository @@ -88,7 +87,6 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, throw throwable } syncResponseHandler.handleResponse(syncResponse, token) - syncTokenStore.saveToken(syncResponse.nextBatch) if (isInitialSync) { initialSyncProgressService.endAll() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt index 350f2a1d83..d0af7d3ff5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.model.SyncEntity -import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import javax.inject.Inject @@ -30,10 +29,8 @@ internal class SyncTokenStore @Inject constructor(private val monarchy: Monarchy } } - suspend fun saveToken(token: String?) { - monarchy.awaitTransaction { - val sync = SyncEntity(token) - it.insertOrUpdate(sync) - } + fun saveToken(realm: Realm, token: String?) { + val sync = SyncEntity(token) + realm.insertOrUpdate(sync) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index 9cc3a5a3c6..3deb612756 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -17,44 +17,39 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.pushrules.RuleScope +import im.vector.matrix.android.api.pushrules.RuleSetKey import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.getDirectRooms +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId -import im.vector.matrix.android.internal.session.pushers.SavePushRulesTask import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.accountdata.* import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper -import im.vector.matrix.android.internal.session.user.accountdata.SaveBreadcrumbsTask -import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm +import io.realm.RealmList import timber.log.Timber import javax.inject.Inject internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy, @UserId private val userId: String, private val directChatsHelper: DirectChatsHelper, - private val updateUserAccountDataTask: UpdateUserAccountDataTask, - private val savePushRulesTask: SavePushRulesTask, - private val saveIgnoredUsersTask: SaveIgnoredUsersTask, - private val saveBreadcrumbsTask: SaveBreadcrumbsTask, - private val taskExecutor: TaskExecutor) { + private val updateUserAccountDataTask: UpdateUserAccountDataTask) { - suspend fun handle(accountData: UserAccountDataSync?, invites: Map?) { + fun handle(realm: Realm, accountData: UserAccountDataSync?) { accountData?.list?.forEach { when (it) { - is UserAccountDataDirectMessages -> handleDirectChatRooms(it) - is UserAccountDataPushRules -> handlePushRules(it) - is UserAccountDataIgnoredUsers -> handleIgnoredUsers(it) - is UserAccountDataBreadcrumbs -> handleBreadcrumbs(it) + is UserAccountDataDirectMessages -> handleDirectChatRooms(realm, it) + is UserAccountDataPushRules -> handlePushRules(realm, it) + is UserAccountDataIgnoredUsers -> handleIgnoredUsers(realm, it) + is UserAccountDataBreadcrumbs -> handleBreadcrumbs(realm, it) is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}") else -> error("Missing code here!") } @@ -65,78 +60,134 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc // it.toString() // MoshiProvider.providesMoshi() // } - - monarchy.doWithRealm { realm -> - synchronizeWithServerIfNeeded(realm, invites) - } - } - - private suspend fun handlePushRules(userAccountDataPushRules: UserAccountDataPushRules) { - savePushRulesTask.execute(SavePushRulesTask.Params(userAccountDataPushRules.content)) - } - - private suspend fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) { - monarchy.awaitTransaction { realm -> - val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) - oldDirectRooms.forEach { - it.isDirect = false - it.directUserId = null - } - directMessages.content.forEach { - val userId = it.key - it.value.forEach { roomId -> - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - if (roomSummaryEntity != null) { - roomSummaryEntity.isDirect = true - roomSummaryEntity.directUserId = userId - realm.insertOrUpdate(roomSummaryEntity) - } - } - } - } } // If we get some direct chat invites, we synchronize the user account data including those. - private fun synchronizeWithServerIfNeeded(realm: Realm, invites: Map?) { + suspend fun synchronizeWithServerIfNeeded(invites: Map) { if (invites.isNullOrEmpty()) return val directChats = directChatsHelper.getLocalUserAccount() var hasUpdate = false - invites.forEach { (roomId, _) -> - val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId) - val inviterId = myUserStateEvent?.sender - val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() } - val isDirect = myUserRoomMember?.isDirect - if (inviterId != null && inviterId != userId && isDirect == true) { - directChats - .getOrPut(inviterId, { arrayListOf() }) - .apply { - if (contains(roomId)) { - Timber.v("Direct chats already include room $roomId with user $inviterId") - } else { - add(roomId) - hasUpdate = true + monarchy.doWithRealm { realm -> + invites.forEach { (roomId, _) -> + val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId) + val inviterId = myUserStateEvent?.sender + val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() } + val isDirect = myUserRoomMember?.isDirect + if (inviterId != null && inviterId != userId && isDirect == true) { + directChats + .getOrPut(inviterId, { arrayListOf() }) + .apply { + if (contains(roomId)) { + Timber.v("Direct chats already include room $roomId with user $inviterId") + } else { + add(roomId) + hasUpdate = true + } } - } + } } } if (hasUpdate) { val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams( directMessages = directChats ) - updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor) + updateUserAccountDataTask.execute(updateUserAccountParams) } } - private fun handleIgnoredUsers(userAccountDataIgnoredUsers: UserAccountDataIgnoredUsers) { - saveIgnoredUsersTask - .configureWith(SaveIgnoredUsersTask.Params(userAccountDataIgnoredUsers.content.ignoredUsers.keys.toList())) - .executeBy(taskExecutor) + + private fun handlePushRules(realm: Realm, userAccountDataPushRules: UserAccountDataPushRules) { + val pushRules = userAccountDataPushRules.content + realm.where(PushRulesEntity::class.java) + .findAll() + .deleteAllFromRealm() + + // Save only global rules for the moment + val globalRules = pushRules.global + + val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT } + globalRules.content?.forEach { rule -> + content.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(content) + + val override = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.OVERRIDE } + globalRules.override?.forEach { rule -> + PushRulesMapper.map(rule).also { + override.pushRules.add(it) + } + } + realm.insertOrUpdate(override) + + val rooms = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.ROOM } + globalRules.room?.forEach { rule -> + rooms.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(rooms) + + val senders = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.SENDER } + globalRules.sender?.forEach { rule -> + senders.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(senders) + + val underrides = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.UNDERRIDE } + globalRules.underride?.forEach { rule -> + underrides.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(underrides) + } + + private fun handleDirectChatRooms(realm: Realm, directMessages: UserAccountDataDirectMessages) { + val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) + oldDirectRooms.forEach { + it.isDirect = false + it.directUserId = null + } + directMessages.content.forEach { + val userId = it.key + it.value.forEach { roomId -> + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummaryEntity != null) { + roomSummaryEntity.isDirect = true + roomSummaryEntity.directUserId = userId + realm.insertOrUpdate(roomSummaryEntity) + } + } + } + } + + private fun handleIgnoredUsers(realm: Realm, userAccountDataIgnoredUsers: UserAccountDataIgnoredUsers) { + val userIds = userAccountDataIgnoredUsers.content.ignoredUsers.keys + realm.where(IgnoredUserEntity::class.java) + .findAll() + .deleteAllFromRealm() + // And save the new received list + userIds.forEach { realm.createObject(IgnoredUserEntity::class.java).apply { userId = it } } // TODO If not initial sync, we should execute a init sync } - private fun handleBreadcrumbs(userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) { - saveBreadcrumbsTask - .configureWith(SaveBreadcrumbsTask.Params(userAccountDataBreadcrumbs.content.recentRoomIds)) - .executeBy(taskExecutor) + private fun handleBreadcrumbs(realm: Realm, userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) { + val recentRoomIds = userAccountDataBreadcrumbs.content.recentRoomIds + val entity = BreadcrumbsEntity.getOrCreate(realm) + + // And save the new received list + entity.recentRoomIds = RealmList().apply { addAll(recentRoomIds) } + + // Update the room summaries + // Reset all the indexes... + RoomSummaryEntity.where(realm) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) + .findAll() + .forEach { + it.breadcrumbsIndex = RoomSummaryEntity.NOT_IN_BREADCRUMBS + } + + // ...and apply new indexes + recentRoomIds.forEachIndexed { index, roomId -> + RoomSummaryEntity.where(realm, roomId) + .findFirst() + ?.breadcrumbsIndex = index + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index 1acb7dbb4a..cf7d3ce63d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -19,21 +19,15 @@ package im.vector.matrix.android.internal.session.sync.job import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.squareup.moshi.JsonEncodingException -import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.session.sync.SyncState -import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.session.sync.SyncTask -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.TaskThread -import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import kotlinx.coroutines.* import timber.log.Timber import java.net.SocketTimeoutException -import java.util.concurrent.CountDownLatch import javax.inject.Inject private const val RETRY_WAIT_TIME_MS = 10_000L diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt index 1df0dae901..7062c63816 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineSequencer.kt @@ -84,6 +84,4 @@ internal open class ChannelCoroutineSequencer : CoroutineSequencer { throw cancellation } } - } - diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt index a367632d9f..9591feaa32 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt @@ -99,7 +99,6 @@ class CoroutineSequencersTest { sequencer.post { suspendingMethod("#1") }.also { results.add(it) } - }, GlobalScope.launch(dispatcher) { val result = sequencer.post { suspendingMethod("#2") }.also { @@ -127,5 +126,4 @@ class CoroutineSequencersTest { println("BLOCKING METHOD $name ENDS on ${Thread.currentThread().name}") return name } - } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt index 9a6905e4c7..3c201542e4 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt @@ -17,13 +17,11 @@ package im.vector.riotx.core.extensions import android.content.Context -import android.content.Intent import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.sync.FilterService -import im.vector.matrix.android.internal.session.sync.job.SyncService import im.vector.riotx.core.services.VectorSyncService import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index b95eddd011..20a71803d8 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -26,7 +26,6 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter -import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.extensions.startSyncing import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.deleteAllFiles From 4c88c12cfe897c7076a9205dd5eda5c6f251a849 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Dec 2019 18:46:19 +0100 Subject: [PATCH 007/105] Initial sync, start the sync thread or the sync service --- .../network/NetworkConnectivityChecker.kt | 2 +- .../android/internal/session/sync/SyncTask.kt | 1 - .../internal/session/sync/job/SyncService.kt | 25 ++++++++++++++++--- .../util/BackgroundDetectionObserver.kt | 6 ++--- .../vector/riotx/core/extensions/Session.kt | 7 +++--- .../riotx/core/services/VectorSyncService.kt | 23 ++++++++--------- .../im/vector/riotx/features/MainActivity.kt | 12 ++++----- vector/src/main/res/values/strings_riotX.xml | 2 ++ 8 files changed, 48 insertions(+), 30 deletions(-) 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 e6dda3f5e1..6565b8685b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -53,7 +53,7 @@ internal class NetworkConnectivityChecker @Inject constructor(private val contex @WorkerThread fun hasInternetAccess(): Boolean { // If we are in background we have unbound merlin, so we have to check - return if (backgroundDetectionObserver.isIsBackground) { + return if (backgroundDetectionObserver.isInBackground) { merlinsBeard.hasInternetAccess() } else { hasInternetAccess diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 0914c35854..189c80d657 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -38,7 +38,6 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, @UserId private val userId: String, private val filterRepository: FilterRepository, private val syncResponseHandler: SyncResponseHandler, - private val sessionParamsStore: SessionParamsStore, private val initialSyncProgressService: DefaultInitialSyncProgressService, private val syncTokenStore: SyncTokenStore, private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, 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 0015308c62..37bcc225c1 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 @@ -19,11 +19,13 @@ import android.app.Service import android.content.Intent import android.os.IBinder import im.vector.matrix.android.api.Matrix -import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.isTokenError +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.* import timber.log.Timber @@ -40,10 +42,13 @@ abstract class SyncService : Service() { private var userId: String? = null private var mIsSelfDestroyed: Boolean = false + private var isInitialSync: Boolean = false + private lateinit var session: Session private lateinit var syncTask: SyncTask private lateinit var networkConnectivityChecker: NetworkConnectivityChecker private lateinit var taskExecutor: TaskExecutor private lateinit var coroutineDispatchers: MatrixCoroutineDispatchers + private lateinit var backgroundDetectionObserver: BackgroundDetectionObserver private val isRunning = AtomicBoolean(false) @@ -52,14 +57,19 @@ abstract class SyncService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Timber.i("onStartCommand $intent") intent?.let { + val matrix = Matrix.getInstance(applicationContext) val safeUserId = it.getStringExtra(EXTRA_USER_ID) ?: return@let - val sessionComponent = Matrix.getInstance(applicationContext).sessionManager.getSessionComponent(safeUserId) + val sessionComponent = matrix.sessionManager.getSessionComponent(safeUserId) ?: return@let + session = sessionComponent.session() userId = safeUserId syncTask = sessionComponent.syncTask() + isInitialSync = !session.hasAlreadySynced() networkConnectivityChecker = sessionComponent.networkConnectivityChecker() taskExecutor = sessionComponent.taskExecutor() coroutineDispatchers = sessionComponent.coroutineDispatchers() + backgroundDetectionObserver = matrix.backgroundDetectionObserver + onStart(isInitialSync) if (isRunning.get()) { Timber.i("Received a start while was already syncing... ignore") } else { @@ -92,7 +102,7 @@ abstract class SyncService : Service() { if (!networkConnectivityChecker.hasInternetAccess()) { Timber.v("No network reschedule to avoid wasting resources") userId?.also { - onRescheduleAsked(it, delay = 10_000L) + onRescheduleAsked(it, isInitialSync, delay = 10_000L) } stopMe() return @@ -101,6 +111,11 @@ abstract class SyncService : Service() { val params = SyncTask.Params(TIME_OUT) try { syncTask.execute(params) + // Start sync if we were doing an initial sync and the syncThread is not launched yet + if (isInitialSync && session.syncState().value == SyncState.Idle) { + val isForeground = !backgroundDetectionObserver.isInBackground + session.startSync(isForeground) + } stopMe() } catch (throwable: Throwable) { Timber.e(throwable) @@ -114,7 +129,9 @@ abstract class SyncService : Service() { } } - abstract fun onRescheduleAsked(userId: String, delay: Long) + abstract fun onStart(isInitialSync: Boolean) + + abstract fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long) override fun onBind(intent: Intent?): IBinder? { return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt index d89b732a0d..b8de50b9fc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt @@ -29,7 +29,7 @@ import javax.inject.Inject @MatrixScope internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver { - var isIsBackground: Boolean = false + var isInBackground: Boolean = false private set private @@ -46,14 +46,14 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse @OnLifecycleEvent(Lifecycle.Event.ON_START) fun onMoveToForeground() { Timber.v("App returning to foreground…") - isIsBackground = false + isInBackground = false listeners.forEach { it.onMoveToForeground() } } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) fun onMoveToBackground() { Timber.v("App going to background…") - isIsBackground = true + isInBackground = true listeners.forEach { it.onMoveToBackground() } } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt index 02615bdf1c..620e32f51f 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt @@ -55,10 +55,11 @@ fun Session.startSyncing(context: Context) { Timber.e(ex) } } + } else { + val isAtLeastStarted = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) + Timber.v("--> is at least started? $isAtLeastStarted") + startSync(isAtLeastStarted) } - val isAtLeastStarted = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) - Timber.v("--> is at least started? $isAtLeastStarted") - startSync(isAtLeastStarted) } /** 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 93c08c50d6..31b56c4e47 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 @@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.session.sync.job.SyncService import im.vector.riotx.R import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.features.notifications.NotificationUtils -import timber.log.Timber class VectorSyncService : SyncService() { @@ -45,16 +44,20 @@ class VectorSyncService : SyncService() { notificationUtils = vectorComponent().notificationUtils() } - /** - * Service is started in fdroid mode when no FCM is available or is used for initialSync - */ - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Timber.v("VectorSyncService - onStartCommand ") + override fun onStart(isInitialSync: Boolean) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notification = notificationUtils.buildForegroundServiceNotification(R.string.notification_listening_for_events, false) + val notificationSubtitleRes = if (isInitialSync) { + R.string.notification_initial_sync + } else { + R.string.notification_listening_for_events + } + val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false) startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) } - return super.onStartCommand(intent, flags, startId) + } + + override fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long) { + reschedule(userId, delay) } override fun onDestroy() { @@ -62,10 +65,6 @@ class VectorSyncService : SyncService() { super.onDestroy() } - override fun onRescheduleAsked(userId: String, delay: Long) { - reschedule(userId, delay) - } - private fun removeForegroundNotif() { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE) diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index 1c8ae3f859..26ecd25178 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -127,7 +127,8 @@ class MainActivity : VectorBaseActivity() { override fun onSuccess(data: Unit) { Timber.w("SIGN_OUT: success, start app") sessionHolder.clearActiveSession() - doLocalCleanupAndStart() + doLocalCleanup() + startNextActivityAndFinish() } override fun onFailure(failure: Throwable) { @@ -137,8 +138,9 @@ class MainActivity : VectorBaseActivity() { args.clearCache -> session.clearCache( object : MatrixCallback { override fun onSuccess(data: Unit) { + doLocalCleanup() session.startSyncing(applicationContext) - doLocalCleanupAndStart() + startNextActivityAndFinish() } override fun onFailure(failure: Throwable) { @@ -153,7 +155,7 @@ class MainActivity : VectorBaseActivity() { Timber.w("Ignoring invalid token global error") } - private fun doLocalCleanupAndStart() { + private fun doLocalCleanup() { GlobalScope.launch(Dispatchers.Main) { // On UI Thread Glide.get(this@MainActivity).clearMemory() @@ -165,8 +167,6 @@ class MainActivity : VectorBaseActivity() { deleteAllFiles(this@MainActivity.cacheDir) } } - - startNextActivityAndFinish() } private fun displayError(failure: Throwable) { @@ -182,7 +182,7 @@ class MainActivity : VectorBaseActivity() { private fun startNextActivityAndFinish() { val intent = when { args.clearCredentials - && !args.isUserLoggedOut -> + && !args.isUserLoggedOut -> // User has explicitly asked to log out LoginActivity.newIntent(this, null) args.isSoftLogout -> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index c9180c3878..1e1eabe602 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -162,4 +162,6 @@ The current session is for user %1$s and you provide credentials for user %2$s. This is not supported by RiotX.\nPlease first clear data, then sign in again on another account. Your matrix.to link was malformed + + Initial Sync… From 7697278bb2ef539e5aaa0987ece51fc9844ade1d Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Dec 2019 16:59:45 +0100 Subject: [PATCH 008/105] LiveObservers: launch directly coroutines --- .../database/RealmLiveEntityObserver.kt | 10 +++++++ .../internal/session/DefaultSession.kt | 29 ++++++++++--------- .../session/group/GroupSummaryUpdater.kt | 28 ++++++++++-------- .../room/EventRelationsAggregationUpdater.kt | 10 +++---- .../create/RoomCreateEventLiveObserver.kt | 25 ++++++++-------- .../session/room/prune/EventsPruner.kt | 12 ++++---- .../RoomTombstoneEventLiveObserver.kt | 29 ++++++++++--------- .../sync/UserAccountDataSyncHandler.kt | 1 - .../internal/session/sync/job/SyncThread.kt | 1 - 9 files changed, 80 insertions(+), 65 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index 9c7a788b44..ee6d8e507c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -19,12 +19,16 @@ package im.vector.matrix.android.internal.database import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.util.createBackgroundHandler import io.realm.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference internal interface LiveEntityObserver { fun start() fun dispose() + fun cancelProcess() fun isStarted(): Boolean } @@ -35,6 +39,7 @@ internal abstract class RealmLiveEntityObserver(protected val r val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND") } + protected val observerScope = CoroutineScope(SupervisorJob()) protected abstract val query: Monarchy.Query private val isStarted = AtomicBoolean(false) private val backgroundRealm = AtomicReference() @@ -59,10 +64,15 @@ internal abstract class RealmLiveEntityObserver(protected val r backgroundRealm.getAndSet(null).also { it.close() } + observerScope.coroutineContext.cancelChildren() } } } + override fun cancelProcess() { + observerScope.coroutineContext.cancelChildren() + } + override fun isStarted(): Boolean { return isStarted.get() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 8d508ccc75..d52379eb6e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -85,19 +85,19 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val initialSyncProgressService: Lazy, private val homeServerCapabilitiesService: Lazy) : Session, - RoomService by roomService.get(), - RoomDirectoryService by roomDirectoryService.get(), - GroupService by groupService.get(), - UserService by userService.get(), - CryptoService by cryptoService.get(), - SignOutService by signOutService.get(), - FilterService by filterService.get(), - PushRuleService by pushRuleService.get(), - PushersService by pushersService.get(), - FileService by fileService.get(), - InitialSyncProgressService by initialSyncProgressService.get(), - SecureStorageService by secureStorageService.get(), - HomeServerCapabilitiesService by homeServerCapabilitiesService.get() { + RoomService by roomService.get(), + RoomDirectoryService by roomDirectoryService.get(), + GroupService by groupService.get(), + UserService by userService.get(), + CryptoService by cryptoService.get(), + SignOutService by signOutService.get(), + FilterService by filterService.get(), + PushRuleService by pushRuleService.get(), + PushersService by pushersService.get(), + FileService by fileService.get(), + InitialSyncProgressService by initialSyncProgressService.get(), + SecureStorageService by secureStorageService.get(), + HomeServerCapabilitiesService by homeServerCapabilitiesService.get() { private var isOpen = false @@ -173,13 +173,14 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se override fun clearCache(callback: MatrixCallback) { stopSync() stopAnyBackgroundSync() + liveEntityObservers.forEach { it.cancelProcess() } cacheService.get().clearCache(callback) } @Subscribe(threadMode = ThreadMode.MAIN) fun onGlobalError(globalError: GlobalError) { if (globalError is GlobalError.InvalidToken - && globalError.softLogout) { + && globalError.softLogout) { // Mark the token has invalid GlobalScope.launch(Dispatchers.IO) { sessionParamsStore.setTokenInvalid(myUserId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index 9eceb44417..553a0387c5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -22,6 +22,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.query.where @@ -31,6 +32,7 @@ import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWor import im.vector.matrix.android.internal.worker.WorkerParamsFactory import io.realm.OrderedCollectionChangeSet import io.realm.RealmResults +import kotlinx.coroutines.launch import javax.inject.Inject private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" @@ -49,14 +51,19 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont .mapNotNull { results[it] } fetchGroupsData(modifiedGroupEntity - .filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE } - .map { it.groupId } - .toList()) + .filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE } + .map { it.groupId } + .toList()) - deleteGroups(modifiedGroupEntity + modifiedGroupEntity .filter { it.membership == Membership.LEAVE } .map { it.groupId } - .toList()) + .toList() + .also { + observerScope.launch { + deleteGroups(it) + } + } } private fun fetchGroupsData(groupIds: List) { @@ -77,12 +84,9 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont /** * Delete the GroupSummaryEntity of left groups */ - private fun deleteGroups(groupIds: List) { - monarchy - .writeAsync { realm -> - GroupSummaryEntity.where(realm, groupIds) - .findAll() - .deleteAllFromRealm() - } + private suspend fun deleteGroups(groupIds: List) = awaitTransaction(monarchy.realmConfiguration) { realm -> + GroupSummaryEntity.where(realm, groupIds) + .findAll() + .deleteAllFromRealm() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index aadf1bfccf..4a14005fe9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -23,11 +23,10 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.UserId -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration import io.realm.RealmResults +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -39,8 +38,7 @@ import javax.inject.Inject internal class EventRelationsAggregationUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, @UserId private val userId: String, - private val task: EventRelationsAggregationTask, - private val taskExecutor: TaskExecutor) : + private val task: EventRelationsAggregationTask) : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { @@ -63,6 +61,8 @@ internal class EventRelationsAggregationUpdater @Inject constructor(@SessionData insertedDomains, userId ) - task.configureWith(params).executeBy(taskExecutor) + observerScope.launch { + task.execute(params) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt index 010023596c..1553ddec04 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -30,9 +31,9 @@ import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import io.realm.OrderedCollectionChangeSet -import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmResults +import kotlinx.coroutines.launch import javax.inject.Inject internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase @@ -51,21 +52,21 @@ internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase } .toList() .also { - handleRoomCreateEvents(it) + observerScope.launch { + handleRoomCreateEvents(it) + } } } - private fun handleRoomCreateEvents(createEvents: List) = Realm.getInstance(realmConfiguration).use { - it.executeTransactionAsync { realm -> - for (event in createEvents) { - val createRoomContent = event.getClearContent().toModel() - val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue + private suspend fun handleRoomCreateEvents(createEvents: List) = awaitTransaction(realmConfiguration) { realm -> + for (event in createEvents) { + val createRoomContent = event.getClearContent().toModel() + val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue - val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() - ?: RoomSummaryEntity(predecessorRoomId) - predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED - realm.insertOrUpdate(predecessorRoomSummary) - } + val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() + ?: RoomSummaryEntity(predecessorRoomId) + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED + realm.insertOrUpdate(predecessorRoomSummary) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index d66a2f6e57..b29d3210bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -23,11 +23,10 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration import io.realm.RealmResults +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -36,8 +35,7 @@ import javax.inject.Inject * As it will actually delete the content, it should be called last in the list of listener. */ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, - private val pruneEventTask: PruneEventTask, - private val taskExecutor: TaskExecutor) : + private val pruneEventTask: PruneEventTask) : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { EventEntity.types(it, listOf(EventType.REDACTION)) } @@ -50,7 +48,9 @@ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfigurat .mapNotNull { results[it]?.asDomain() } .toList() - val params = PruneEventTask.Params(insertedDomains) - pruneEventTask.configureWith(params).executeBy(taskExecutor) + observerScope.launch { + val params = PruneEventTask.Params(insertedDomains) + pruneEventTask.execute(params) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt index 301c383a6d..e5e538ae89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -30,9 +31,9 @@ import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import io.realm.OrderedCollectionChangeSet -import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmResults +import kotlinx.coroutines.launch import javax.inject.Inject internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase @@ -51,24 +52,24 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba } .toList() .also { - handleRoomTombstoneEvents(it) + observerScope.launch { + handleRoomTombstoneEvents(it) + } } } - private fun handleRoomTombstoneEvents(tombstoneEvents: List) = Realm.getInstance(realmConfiguration).use { - it.executeTransactionAsync { realm -> - for (event in tombstoneEvents) { - if (event.roomId == null) continue - val createRoomContent = event.getClearContent().toModel() - if (createRoomContent?.replacementRoom == null) continue + private suspend fun handleRoomTombstoneEvents(tombstoneEvents: List) = awaitTransaction(realmConfiguration) { realm -> + for (event in tombstoneEvents) { + if (event.roomId == null) continue + val createRoomContent = event.getClearContent().toModel() + if (createRoomContent?.replacementRoom == null) continue - val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() - ?: RoomSummaryEntity(event.roomId) - if (predecessorRoomSummary.versioningState == VersioningState.NONE) { - predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED - } - realm.insertOrUpdate(predecessorRoomSummary) + val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() + ?: RoomSummaryEntity(event.roomId) + if (predecessorRoomSummary.versioningState == VersioningState.NONE) { + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED } + realm.insertOrUpdate(predecessorRoomSummary) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index 3deb612756..35988e6c6f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -95,7 +95,6 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc } } - private fun handlePushRules(realm: Realm, userAccountDataPushRules: UserAccountDataPushRules) { val pushRules = userAccountDataPushRules.content realm.where(PushRulesEntity::class.java) 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 8b82e13d74..c550d40e1e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.squareup.moshi.JsonEncodingException import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.isTokenError import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.internal.network.NetworkConnectivityChecker From c8f0c83cd3b356e7af6af9244ae1413ec0db63db Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Dec 2019 10:38:10 +0100 Subject: [PATCH 009/105] Timeline: don't retry automatically to avoid totally blocking pagination --- .../internal/session/room/timeline/DefaultTimeline.kt | 3 ++- .../home/room/detail/timeline/TimelineEventController.kt | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 693855edbc..f42e50bdab 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 @@ -504,7 +504,6 @@ internal class DefaultTimeline( Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask .configureWith(params) { - this.retryCount = Int.MAX_VALUE this.constraints = TaskConstraints(connectedToNetwork = true) this.callback = object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { @@ -524,6 +523,8 @@ internal class DefaultTimeline( } override fun onFailure(failure: Throwable) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + postSnapshot() Timber.v("Failure fetching $limit items $direction from pagination request") } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 576b9fa0ba..0f187fa9e5 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 @@ -220,7 +220,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec showingForwardLoader = LoadingItem_() .id("forward_loading_item_$timestamp") .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS) - .addWhen(Timeline.Direction.FORWARDS) + .addWhenLoading(Timeline.Direction.FORWARDS) val timelineModels = getModels() add(timelineModels) @@ -230,7 +230,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec LoadingItem_() .id("backward_loading_item_$timestamp") .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS) - .addWhen(Timeline.Direction.BACKWARDS) + .addWhenLoading(Timeline.Direction.BACKWARDS) } } @@ -247,6 +247,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec currentSnapshot = newSnapshot val diffResult = DiffUtil.calculateDiff(diffCallback) diffResult.dispatchUpdatesTo(listUpdateCallback) + requestDelayedModelBuild(100) inSubmitList = false } } @@ -319,7 +320,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec /** * Return true if added */ - private fun LoadingItem_.addWhen(direction: Timeline.Direction): Boolean { + private fun LoadingItem_.addWhenLoading(direction: Timeline.Direction): Boolean { val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false addIf(shouldAdd, this@TimelineEventController) return shouldAdd From 5bde7b9f178f7d4f0e40e495604f8083a32705d1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Dec 2019 10:37:31 +0100 Subject: [PATCH 010/105] Read marker: fix banner visibility when following permalink --- .../features/home/room/detail/RoomDetailViewModel.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index e7a18753cd..614a75265d 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 @@ -819,9 +819,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro if (events.isEmpty()) return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) - ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) - ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) + if (firstDisplayableEventId == null || firstDisplayableEventIndex == null) { + return if (timeline.isLive) { + UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) + } else { + UnreadState.Unknown + } + } for (i in (firstDisplayableEventIndex - 1) downTo 0) { val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown From a4ea9a09ad2fe44bc6f867d939d060be9a4fa44e Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Dec 2019 13:41:57 +0100 Subject: [PATCH 011/105] Room factory: add scope to avoid recreate all the dependencies --- .../vector/matrix/android/internal/session/room/RoomFactory.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 30a2948f68..a21a3b4a8d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper +import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService @@ -35,6 +36,7 @@ internal interface RoomFactory { fun create(roomId: String): Room } +@SessionScope internal class DefaultRoomFactory @Inject constructor(private val monarchy: Monarchy, private val roomSummaryMapper: RoomSummaryMapper, private val cryptoService: CryptoService, From 7bb8cb0682880010b00ef553f48d609464760125 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Dec 2019 13:42:15 +0100 Subject: [PATCH 012/105] Permalink: fix nav to same room --- .../java/im/vector/riotx/features/permalink/PermalinkHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt index c849166738..41ddb49cb5 100644 --- a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt @@ -57,7 +57,7 @@ class PermalinkHandler @Inject constructor(private val session: Session, .observeOn(AndroidSchedulers.mainThread()) .map { val roomId = it.getOrNull() - if (navigateToRoomInterceptor?.navToRoom(roomId) != true) { + if (navigateToRoomInterceptor?.navToRoom(roomId, permalinkData.eventId) != true) { openRoom(context, roomId, permalinkData.eventId, buildTask) } true From a6afd2e90402b3fa9676deb4f6ac2a23d8ba01fd Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 9 Dec 2019 19:26:20 +0100 Subject: [PATCH 013/105] Timeline: handle failure when navigating to an unknown event (+ clean some files) --- .../session/room/timeline/TimelineTest.kt | 2 +- .../api/session/room/timeline/Timeline.kt | 9 +++++-- .../database/helper/ChunkEntityHelper.kt | 7 +----- .../session/room/RoomSummaryUpdater.kt | 6 ++--- .../session/room/timeline/DefaultTimeline.kt | 25 +++++++++++++++++-- .../room/timeline/TokenChunkEventPersistor.kt | 10 +++++--- .../android/internal/session/sync/SyncTask.kt | 1 - .../riotx/core/platform/VectorBaseFragment.kt | 8 ++++++ .../home/room/detail/RoomDetailFragment.kt | 10 ++++++++ .../home/room/detail/RoomDetailViewEvents.kt | 24 ++++++++++++++++++ .../home/room/detail/RoomDetailViewModel.kt | 14 ++++++++++- .../timeline/TimelineEventController.kt | 6 ++++- .../home/room/list/RoomListFragment.kt | 9 +------ 13 files changed, 102 insertions(+), 29 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index 06651f9ba3..008508ae19 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -66,7 +66,7 @@ internal class TimelineTest : InstrumentedTest { // val latch = CountDownLatch(2) // var timelineEvents: List = emptyList() // timeline.listener = object : Timeline.Listener { -// override fun onUpdated(snapshot: List) { +// override fun onTimelineUpdated(snapshot: List) { // if (snapshot.isNotEmpty()) { // if (initialLoad == 0) { // initialLoad = snapshot.size diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 85dbdcaa19..2280803e5c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -65,7 +65,7 @@ interface Timeline { /** * This is the main method to enrich the timeline with new data. - * It will call the onUpdated method from [Listener] when the data will be processed. + * It will call the onTimelineUpdated method from [Listener] when the data will be processed. * It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row. */ fun paginate(direction: Direction, count: Int) @@ -106,7 +106,12 @@ interface Timeline { * Call when the timeline has been updated through pagination or sync. * @param snapshot the most up to date snapshot */ - fun onUpdated(snapshot: List) + fun onTimelineUpdated(snapshot: List) + + /** + * Called whenever an error we can't recover from occurred + */ + fun onTimelineFailure(throwable: Throwable) } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 826b35254e..f05fa01444 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -21,12 +21,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 126d13c5db..509beddbf0 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 @@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.query.* import im.vector.matrix.android.internal.database.query.isEventRead import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.prev @@ -38,7 +39,6 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications import io.realm.Realm -import io.realm.kotlin.createObject import javax.inject.Inject internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId: String, @@ -69,9 +69,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId roomSummary: RoomSyncSummary? = null, unreadNotifications: RoomSyncUnreadNotifications? = null, updateMembers: Boolean = false) { - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) - + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) if (roomSummary != null) { if (roomSummary.heroes.isNotEmpty()) { roomSummaryEntity.heroes.clear() 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 f42e50bdab..85bab5d706 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 @@ -638,7 +638,14 @@ internal class DefaultTimeline( private fun fetchEvent(eventId: String) { val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize) - cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor) + cancelableBag += contextOfEventTask.configureWith(params) { + callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + postFailure(failure) + } + } + } + .executeBy(taskExecutor) } private fun postSnapshot() { @@ -651,7 +658,7 @@ internal class DefaultTimeline( val runnable = Runnable { synchronized(listeners) { listeners.forEach { - it.onUpdated(snapshot) + it.onTimelineUpdated(snapshot) } } } @@ -659,6 +666,20 @@ internal class DefaultTimeline( } } + private fun postFailure(throwable: Throwable) { + if (isReady.get().not()) { + return + } + val runnable = Runnable { + synchronized(listeners) { + listeners.forEach { + it.onTimelineFailure(throwable) + } + } + } + mainHandler.post(runnable) + } + private fun clearAllValues() { prevDisplayIndex = null nextDisplayIndex = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 0d9fb4e9e6..7030509bfc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -18,6 +18,10 @@ package im.vector.matrix.android.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.addStateEvent +import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.create @@ -112,7 +116,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) val nextToken: String? val prevToken: String? @@ -125,7 +129,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy } val shouldSkip = ChunkEntity.find(realm, roomId, nextToken = nextToken) != null - || ChunkEntity.find(realm, roomId, prevToken = prevToken) != null + || ChunkEntity.find(realm, roomId, prevToken = prevToken) != null val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken) val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken) @@ -139,7 +143,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy } else { nextChunk?.apply { this.prevToken = prevToken } } - ?: ChunkEntity.create(realm, prevToken, nextToken) + ?: ChunkEntity.create(realm, prevToken, nextToken) if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { Timber.v("Reach end of $roomId") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 189c80d657..d99b9df4df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.R -import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService 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 efcbdfff39..902dfd33a7 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -31,6 +31,7 @@ import butterknife.Unbinder import com.airbnb.mvrx.BaseMvRxFragment import com.airbnb.mvrx.MvRx import com.bumptech.glide.util.Util.assertMainThread +import com.google.android.material.snackbar.Snackbar import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.ScreenComponent @@ -167,6 +168,13 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { return this } + protected fun showErrorInSnackbar(throwable: Throwable) { + vectorBaseActivity.coordinatorLayout?.let { + Snackbar.make(it, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT) + .show() + } + } + /* ========================================================================================== * Toolbar * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 8887b94f92..64544a9035 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 @@ -274,6 +274,16 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.requestLiveData.observeEvent(this) { displayRoomDetailActionResult(it) } + + roomDetailViewModel.viewEvents + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + when (it) { + is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + } + } + .disposeOnDestroyView() } override fun onActivityCreated(savedInstanceState: Bundle?) { 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 new file mode 100644 index 0000000000..a1ad480584 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail + +/** + * Transient events for RoomDetail + */ +sealed class RoomDetailViewEvents { + data class Failure(val throwable: Throwable) : RoomDetailViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 614a75265d..467148302f 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 @@ -27,6 +27,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.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isImageMessage @@ -56,7 +57,9 @@ import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.UserPreferencesProvider +import im.vector.riotx.core.utils.DataSource import im.vector.riotx.core.utils.LiveEvent +import im.vector.riotx.core.utils.PublishDataSource import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand @@ -101,6 +104,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private var timelineEvents = PublishRelay.create>() private var timeline = room.createTimeline(eventId, timelineSettings) + private val _viewEvents = PublishDataSource() + val viewEvents: DataSource = _viewEvents + // Can be used for several actions, for a one shot result private val _requestLiveData = MutableLiveData>>() val requestLiveData: LiveData>> @@ -862,10 +868,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - override fun onUpdated(snapshot: List) { + override fun onTimelineUpdated(snapshot: List) { timelineEvents.accept(snapshot) } + override fun onTimelineFailure(throwable: Throwable) { + // If we have a critical timeline issue, we get back to live. + timeline.restartWithEventId(null) + _viewEvents.post(RoomDetailViewEvents.Failure(throwable)) + } + override fun onCleared() { timeline.dispose() timeline.removeListener(this) 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 0f187fa9e5..582544ce8a 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 @@ -236,10 +236,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Timeline.LISTENER *************************************************************************** - override fun onUpdated(snapshot: List) { + override fun onTimelineUpdated(snapshot: List) { submitSnapshot(snapshot) } + override fun onTimelineFailure(throwable: Throwable) { + // no-op, already handled + } + private fun submitSnapshot(newSnapshot: List) { backgroundHandler.post { inSubmitList = true diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 9e54d5fc79..27fbc387b1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -104,7 +104,7 @@ class RoomListFragment @Inject constructor( .subscribe { when (it) { is RoomListViewEvents.SelectRoom -> openSelectedRoom(it) - is RoomListViewEvents.Failure -> showError(it) + is RoomListViewEvents.Failure -> showErrorInSnackbar(it.throwable) } } .disposeOnDestroyView() @@ -135,13 +135,6 @@ class RoomListFragment @Inject constructor( } } - private fun showError(event: RoomListViewEvents.Failure) { - vectorBaseActivity.coordinatorLayout?.let { - Snackbar.make(it, errorFormatter.toHumanReadable(event.throwable), Snackbar.LENGTH_SHORT) - .show() - } - } - private fun setupCreateRoomButton() { when (roomListParams.displayMode) { RoomListDisplayMode.HOME -> createChatFabMenu.isVisible = true From 0eb0870d6c7542ce493d18fa92159491a6fa06c8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Dec 2019 18:29:46 +0100 Subject: [PATCH 014/105] AvatarRenderer: allow to pass GlideRequests too (fix home group avatar) --- .../java/im/vector/riotx/features/home/AvatarRenderer.kt | 8 ++++++++ .../im/vector/riotx/features/home/HomeDetailFragment.kt | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index 4e1808a48a..b7c3e61ee4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -52,6 +52,14 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active DrawableImageViewTarget(imageView)) } + @UiThread + fun render(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) { + render(imageView.context, + glideRequests, + matrixItem, + DrawableImageViewTarget(imageView)) + } + @UiThread fun render(context: Context, glideRequest: GlideRequests, diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index fc0eeaf92c..b9d3e3c95e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.extensions.commitTransactionNow +import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.KeysBackupBanner @@ -75,7 +76,8 @@ class HomeDetailFragment @Inject constructor( private fun onGroupChange(groupSummary: GroupSummary?) { groupSummary?.let { - avatarRenderer.render(it.toMatrixItem(), groupToolbarAvatarImageView) + // Use GlideApp with activity context to avoid the glideRequests to be paused + avatarRenderer.render(it.toMatrixItem(), groupToolbarAvatarImageView, GlideApp.with(requireActivity())) } } From e73970d61b263eefba70c16e5ac9c56c95361504 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 19:39:35 +0100 Subject: [PATCH 015/105] Render aliases and canonical alias change in the timeline --- CHANGES.md | 2 +- .../src/main/res/values/strings_RiotX.xml | 15 +++++++++- .../action/MessageActionsViewModel.kt | 2 ++ .../timeline/factory/TimelineItemFactory.kt | 2 ++ .../timeline/format/NoticeEventFormatter.kt | 30 +++++++++++++++++++ .../helper/MessageInformationDataFactory.kt | 1 + .../helper/TimelineDisplayableEvents.kt | 2 ++ 7 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 863b1ca455..89c1942f39 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features ✨: - Improvements 🙌: - - + - Render aliases and canonical alias change in the timeline Other changes: - diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml index 03bc6d3684..84aa9d9f5c 100644 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml @@ -2,6 +2,19 @@ + + %1$s added %2$s as an address for this room. + %1$s added %2$s as addresses for this room. + + + %1$s removed %2$s as an address for this room. + %1$s removed %3$s as addresses for this room. + - \ No newline at end of file + %1$s added %2$s and removed %3$s as addresses for this room. + + "%1$s set the main address for this room to %2$s." + "%1$s removed the main address for this room." + + diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 1303c3aad9..be969fd532 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 @@ -187,6 +187,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_ALIASES, + EventType.STATE_CANONICAL_ALIAS, EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 5b6dec9900..1c1155d7b2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -45,6 +45,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_ALIASES, + EventType.STATE_CANONICAL_ALIAS, EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 75100e6c03..2bf861a970 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -37,6 +37,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_INVITE, @@ -136,6 +138,34 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active } } + private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? { + val eventContent: RoomAliasesContent? = event.getClearContent().toModel() + val prevEventContent: RoomAliasesContent? = event.unsignedData?.prevContent?.toModel() + + val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty() + val removedAliases = prevEventContent?.aliases.orEmpty() - eventContent?.aliases.orEmpty() + + return if (addedAliases.isNotEmpty() && removedAliases.isNotEmpty()) { + sp.getString(R.string.notice_room_aliases_added_and_removed, senderName, addedAliases.joinToString(), removedAliases.joinToString()) + } else if (addedAliases.isNotEmpty()) { + sp.getQuantityString(R.plurals.notice_room_aliases_added, addedAliases.size, senderName, addedAliases.joinToString()) + } else if (removedAliases.isNotEmpty()) { + sp.getQuantityString(R.plurals.notice_room_aliases_removed, removedAliases.size, senderName, removedAliases.joinToString()) + } else { + Timber.w("Alias event without any change...") + null + } + } + + private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? { + val eventContent: RoomCanonicalAliasContent? = event.getClearContent().toModel() + val canonicalAlias = eventContent?.canonicalAlias + return canonicalAlias + ?.takeIf { it.isNotBlank() } + ?.let { sp.getString(R.string.notice_room_canonical_alias_set, senderName, it) } + ?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName) + } + private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String { val displayText = StringBuilder() // Check display name has been changed diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 3331fbf774..7ee8486ba2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -33,6 +33,7 @@ import me.gujun.android.span.span import javax.inject.Inject /** + * TODO Update this comment * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline */ class MessageInformationDataFactory @Inject constructor(private val session: Session, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 1cd851f8c8..4bb0fc27d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -27,6 +27,8 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_ALIASES, + EventType.STATE_CANONICAL_ALIAS, EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, From 07817b69c24058ecdc775e2d6ad9fe3df49a7bbc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 19:46:09 +0100 Subject: [PATCH 016/105] Rename some event type --- .../api/session/events/model/EventType.kt | 14 +++---- .../room/model/RoomCanonicalAliasContent.kt | 2 +- .../room/model/create/CreateRoomParams.kt | 4 +- .../internal/crypto/DefaultCryptoService.kt | 12 +++--- .../session/room/RoomSummaryUpdater.kt | 4 +- .../membership/RoomDisplayNameResolver.kt | 2 +- .../session/room/prune/PruneEventTask.kt | 16 ++++---- .../action/MessageActionsViewModel.kt | 4 +- .../timeline/factory/TimelineItemFactory.kt | 4 +- .../timeline/format/NoticeEventFormatter.kt | 38 +++++++++---------- .../helper/TimelineDisplayableEvents.kt | 4 +- 11 files changed, 52 insertions(+), 52 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 38c24fa89b..bca2ded8bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -50,10 +50,10 @@ object EventType { const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" const val STATE_ROOM_ALIASES = "m.room.aliases" const val STATE_ROOM_TOMBSTONE = "m.room.tombstone" - const val STATE_CANONICAL_ALIAS = "m.room.canonical_alias" - const val STATE_HISTORY_VISIBILITY = "m.room.history_visibility" - const val STATE_RELATED_GROUPS = "m.room.related_groups" - const val STATE_PINNED_EVENT = "m.room.pinned_events" + const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias" + const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility" + const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups" + const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" // Call Events @@ -87,9 +87,9 @@ object EventType { STATE_ROOM_GUEST_ACCESS, STATE_ROOM_POWER_LEVELS, STATE_ROOM_TOMBSTONE, - STATE_HISTORY_VISIBILITY, - STATE_RELATED_GROUPS, - STATE_PINNED_EVENT + STATE_ROOM_HISTORY_VISIBILITY, + STATE_ROOM_RELATED_GROUPS, + STATE_ROOM_PINNED_EVENT ) fun isStateEvent(type: String): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt index a66f23555d..0aec7f6c8b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomCanonicalAliasContent.kt @@ -20,7 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass /** - * Class representing the EventType.STATE_CANONICAL_ALIAS state event content + * Class representing the EventType.STATE_ROOM_CANONICAL_ALIAS state event content */ @JsonClass(generateAdapter = true) data class RoomCanonicalAliasContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index 598aab2d30..bc1e941698 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -145,13 +145,13 @@ class CreateRoomParams { */ fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) { // Remove the existing value if any. - initialStates?.removeAll { it.getClearType() == EventType.STATE_HISTORY_VISIBILITY } + initialStates?.removeAll { it.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY } if (historyVisibility != null) { val contentMap = HashMap() contentMap["history_visibility"] = historyVisibility - val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY, + val historyVisibilityEvent = Event(type = EventType.STATE_ROOM_HISTORY_VISIBILITY, stateKey = "", content = contentMap.toContent()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index c50b9e2e10..28a9fad35f 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 @@ -145,17 +145,17 @@ internal class DefaultCryptoService @Inject constructor( fun onStateEvent(roomId: String, event: Event) { when { - event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } } fun onLiveEvent(roomId: String, event: Event) { when { - event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 126d13c5db..c4d0d50283 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 @@ -52,7 +52,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, - EventType.STATE_HISTORY_VISIBILITY, + EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, @@ -93,7 +93,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() - val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev() + val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev() val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 2271631932..21270308ed 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 @@ -62,7 +62,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return@doWithRealm } - val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev() + val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev() name = ContentMapper.map(canonicalAlias?.content).toModel()?.canonicalAlias if (!name.isNullOrEmpty()) { return@doWithRealm diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt index c303e1c215..de3eb1eab2 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 @@ -105,10 +105,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M private fun computeAllowedKeys(type: String): List { // Add filtered content, allowed keys in content depends on the event type return when (type) { - EventType.STATE_ROOM_MEMBER -> listOf("membership") - EventType.STATE_ROOM_CREATE -> listOf("creator") - EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") - EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", + EventType.STATE_ROOM_MEMBER -> listOf("membership") + EventType.STATE_ROOM_CREATE -> listOf("creator") + EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") + EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", "users_default", "events", "events_default", @@ -117,10 +117,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M "kick", "redact", "invite") - EventType.STATE_ROOM_ALIASES -> listOf("aliases") - EventType.STATE_CANONICAL_ALIAS -> listOf("alias") - EventType.FEEDBACK -> listOf("type", "target_event_id") - else -> emptyList() + EventType.STATE_ROOM_ALIASES -> listOf("aliases") + EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias") + EventType.FEEDBACK -> listOf("type", "target_event_id") + else -> emptyList() } } } diff --git a/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 be969fd532..d537b66ec3 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 @@ -188,8 +188,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, - EventType.STATE_CANONICAL_ALIAS, - EventType.STATE_HISTORY_VISIBILITY, + EventType.STATE_ROOM_CANONICAL_ALIAS, + EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 1c1155d7b2..4a7a1e2a86 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -46,9 +46,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, - EventType.STATE_CANONICAL_ALIAS, + EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_JOIN_RULES, - EventType.STATE_HISTORY_VISIBILITY, + EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 2bf861a970..9cb045c01e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -33,21 +33,21 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active fun format(timelineEvent: TimelineEvent): CharSequence? { return when (val type = timelineEvent.root.getClearType()) { - EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_INVITE, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.MESSAGE, EventType.REACTION, - EventType.REDACTION -> formatDebug(timelineEvent.root) - else -> { + EventType.REDACTION -> formatDebug(timelineEvent.root) + else -> { Timber.v("Type $type not handled by this formatter") null } @@ -56,16 +56,16 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active fun format(event: Event, senderName: String?): CharSequence? { return when (val type = event.getClearType()) { - EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName) - EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) - EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) - EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName) - EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName) + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName) + EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) + EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) + EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName) EventType.CALL_INVITE, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> formatCallEvent(event, senderName) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(senderName) - else -> { + EventType.CALL_ANSWER -> formatCallEvent(event, senderName) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(senderName) + else -> { Timber.v("Type $type not handled by this formatter") null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 4bb0fc27d5..b0f3e617a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -28,8 +28,8 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, - EventType.STATE_CANONICAL_ALIAS, - EventType.STATE_HISTORY_VISIBILITY, + EventType.STATE_ROOM_CANONICAL_ALIAS, + EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, From d342356f29ea58300aebb815b1438663cc8e7e9c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 19:48:30 +0100 Subject: [PATCH 017/105] Add missing state events to the list (not sure about the side effects) --- .../vector/matrix/android/api/session/events/model/EventType.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index bca2ded8bc..22bf564a8a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -86,7 +86,9 @@ object EventType { STATE_ROOM_JOIN_RULES, STATE_ROOM_GUEST_ACCESS, STATE_ROOM_POWER_LEVELS, + STATE_ROOM_ALIASES, STATE_ROOM_TOMBSTONE, + STATE_ROOM_CANONICAL_ALIAS, STATE_ROOM_HISTORY_VISIBILITY, STATE_ROOM_RELATED_GROUPS, STATE_ROOM_PINNED_EVENT From c18be94986f9e77930a96cce0eccf26649af4185 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 20:03:10 +0100 Subject: [PATCH 018/105] Fix lots of trouble with the completion popup (resize, change mode, etc.) --- .../autocomplete/EpoxyAutocompletePresenter.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt index 227f1b2f9c..0862fc059f 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt @@ -19,18 +19,18 @@ package im.vector.riotx.features.autocomplete import android.content.Context import android.database.DataSetObserver import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController -import com.airbnb.epoxy.EpoxyRecyclerView import com.otaliastudios.autocomplete.AutocompletePresenter abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePresenter(context), AutocompleteClickListener { - private var recyclerView: EpoxyRecyclerView? = null - private var clicks: AutocompletePresenter.ClickProvider? = null + private var recyclerView: RecyclerView? = null + private var clicks: ClickProvider? = null private var observer: Observer? = null - override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider) { + override fun registerClickProvider(provider: ClickProvider) { this.clicks = provider } @@ -39,8 +39,10 @@ abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePre } override fun getView(): ViewGroup? { - recyclerView = EpoxyRecyclerView(context).apply { - setController(providesController()) + recyclerView = RecyclerView(context).apply { + layoutManager = LinearLayoutManager(context) + setHasFixedSize(false) + adapter = providesController().adapter observer?.let { adapter?.registerAdapterDataObserver(it) } @@ -52,6 +54,7 @@ abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePre override fun onViewShown() {} override fun onViewHidden() { + recyclerView?.adapter = null recyclerView = null observer = null } From 237b22df5923376275c34018b4a0398e780be678 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Dec 2019 20:31:36 +0100 Subject: [PATCH 019/105] Fix lots of trouble with the completion popup (resize, change mode, etc.) - next step --- .../EpoxyAutocompletePresenter.kt | 94 ------------------- .../command/AutocompleteCommandPresenter.kt | 15 ++- .../user/AutocompleteUserPresenter.kt | 15 ++- 3 files changed, 20 insertions(+), 104 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt deleted file mode 100644 index 0862fc059f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/EpoxyAutocompletePresenter.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.autocomplete - -import android.content.Context -import android.database.DataSetObserver -import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.airbnb.epoxy.EpoxyController -import com.otaliastudios.autocomplete.AutocompletePresenter - -abstract class EpoxyAutocompletePresenter(context: Context) : AutocompletePresenter(context), AutocompleteClickListener { - - private var recyclerView: RecyclerView? = null - private var clicks: ClickProvider? = null - private var observer: Observer? = null - - override fun registerClickProvider(provider: ClickProvider) { - this.clicks = provider - } - - override fun registerDataSetObserver(observer: DataSetObserver) { - this.observer = Observer(observer) - } - - override fun getView(): ViewGroup? { - recyclerView = RecyclerView(context).apply { - layoutManager = LinearLayoutManager(context) - setHasFixedSize(false) - adapter = providesController().adapter - observer?.let { - adapter?.registerAdapterDataObserver(it) - } - itemAnimator = null - } - return recyclerView - } - - override fun onViewShown() {} - - override fun onViewHidden() { - recyclerView?.adapter = null - recyclerView = null - observer = null - } - - abstract fun providesController(): EpoxyController - - protected fun dispatchLayoutChange() { - observer?.onChanged() - } - - override fun onItemClick(t: T) { - clicks?.click(t) - } - - private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() { - - override fun onChanged() { - root.onChanged() - } - - override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { - root.onChanged() - } - - override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { - root.onChanged() - } - - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - root.onChanged() - } - - override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { - root.onChanged() - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt index 915689fbeb..2f076c4c53 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -17,21 +17,26 @@ package im.vector.riotx.features.autocomplete.command import android.content.Context -import com.airbnb.epoxy.EpoxyController -import im.vector.riotx.features.autocomplete.EpoxyAutocompletePresenter +import androidx.recyclerview.widget.RecyclerView +import com.otaliastudios.autocomplete.RecyclerViewPresenter +import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.command.Command import javax.inject.Inject class AutocompleteCommandPresenter @Inject constructor(context: Context, private val controller: AutocompleteCommandController) : - EpoxyAutocompletePresenter(context) { + RecyclerViewPresenter(context), AutocompleteClickListener { init { controller.listener = this } - override fun providesController(): EpoxyController { - return controller + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + return controller.adapter + } + + override fun onItemClick(t: Command) { + dispatchClick(t) } override fun onQuery(query: CharSequence?) { diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt index 5c2d2c49c0..d60ea5db67 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt @@ -17,16 +17,17 @@ package im.vector.riotx.features.autocomplete.user import android.content.Context -import com.airbnb.epoxy.EpoxyController +import androidx.recyclerview.widget.RecyclerView import com.airbnb.mvrx.Async import com.airbnb.mvrx.Success +import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.matrix.android.api.session.user.model.User -import im.vector.riotx.features.autocomplete.EpoxyAutocompletePresenter +import im.vector.riotx.features.autocomplete.AutocompleteClickListener import javax.inject.Inject class AutocompleteUserPresenter @Inject constructor(context: Context, private val controller: AutocompleteUserController -) : EpoxyAutocompletePresenter(context) { +) : RecyclerViewPresenter(context), AutocompleteClickListener { var callback: Callback? = null @@ -34,8 +35,12 @@ class AutocompleteUserPresenter @Inject constructor(context: Context, controller.listener = this } - override fun providesController(): EpoxyController { - return controller + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + return controller.adapter + } + + override fun onItemClick(t: User) { + dispatchClick(t) } override fun onQuery(query: CharSequence?) { From 3a829bdfe85d75bffd59fe6a4a10fb9b1f4d1c96 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 00:05:04 +0100 Subject: [PATCH 020/105] Fix command truncation --- vector/src/main/res/layout/item_autocomplete_command.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/item_autocomplete_command.xml b/vector/src/main/res/layout/item_autocomplete_command.xml index 0d858f0e02..4fe815fbed 100644 --- a/vector/src/main/res/layout/item_autocomplete_command.xml +++ b/vector/src/main/res/layout/item_autocomplete_command.xml @@ -41,7 +41,7 @@ android:layout_alignParentStart="true" android:layout_alignParentLeft="true" android:layout_gravity="center_vertical" - android:maxLines="1" + android:maxLines="2" android:textColor="?riotx_text_secondary" android:textSize="12sp" tools:text="@string/command_description_invite_user" /> From 92f43a591acac58455cbb0f62f1e1d654cc70ad5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 00:38:42 +0100 Subject: [PATCH 021/105] Autocompletion for room canonical alias --- .../matrix/android/api/util/MatrixItem.kt | 8 ++- ...eUserItem.kt => AutocompleteMatrixItem.kt} | 14 +++-- .../room/AutocompleteRoomController.kt | 49 +++++++++++++++ .../room/AutocompleteRoomPresenter.kt | 59 +++++++++++++++++++ .../user/AutocompleteUserController.kt | 3 +- .../home/room/detail/RoomDetailFragment.kt | 58 ++++++++++++++++++ .../detail/composer/TextComposerAction.kt | 1 + .../detail/composer/TextComposerViewModel.kt | 38 ++++++++++-- .../detail/composer/TextComposerViewState.kt | 4 +- .../layout/item_autocomplete_matrix_item.xml | 51 ++++++++++++++++ .../res/layout/item_autocomplete_user.xml | 30 ---------- 11 files changed, 272 insertions(+), 43 deletions(-) rename vector/src/main/java/im/vector/riotx/features/autocomplete/{user/AutocompleteUserItem.kt => AutocompleteMatrixItem.kt} (71%) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt create mode 100644 vector/src/main/res/layout/item_autocomplete_matrix_item.xml delete mode 100644 vector/src/main/res/layout/item_autocomplete_user.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index 4fed773ae2..1ca7237d3d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt @@ -62,6 +62,9 @@ sealed class MatrixItem( init { if (BuildConfig.DEBUG) checkId() } + + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + override fun getBestName() = id } data class GroupItem(override val id: String, @@ -73,7 +76,7 @@ sealed class MatrixItem( } } - fun getBestName(): String { + open fun getBestName(): String { return displayName?.takeIf { it.isNotBlank() } ?: id } @@ -95,7 +98,7 @@ sealed class MatrixItem( } fun firstLetterOfDisplayName(): String { - return getBestName() + return (displayName?.takeIf { it.isNotBlank() } ?: id) .let { dn -> var startIndex = 0 val initial = dn[startIndex] @@ -138,4 +141,5 @@ sealed class MatrixItem( fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl) fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) +fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl) diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/AutocompleteMatrixItem.kt similarity index 71% rename from vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt rename to vector/src/main/java/im/vector/riotx/features/autocomplete/AutocompleteMatrixItem.kt index 8581ba8e2c..d5eb90a62c 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/AutocompleteMatrixItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.autocomplete.user +package im.vector.riotx.features.autocomplete import android.view.View import android.widget.ImageView @@ -25,23 +25,27 @@ import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.features.home.AvatarRenderer -@EpoxyModelClass(layout = R.layout.item_autocomplete_user) -abstract class AutocompleteUserItem : VectorEpoxyModel() { +@EpoxyModelClass(layout = R.layout.item_autocomplete_matrix_item) +abstract class AutocompleteMatrixItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute var subName: String? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null override fun bind(holder: Holder) { holder.view.setOnClickListener(clickListener) holder.nameView.text = matrixItem.getBestName() + holder.subNameView.setTextOrHide(subName) avatarRenderer.render(matrixItem, holder.avatarImageView) } class Holder : VectorEpoxyHolder() { - val nameView by bind(R.id.userAutocompleteName) - val avatarImageView by bind(R.id.userAutocompleteAvatar) + val nameView by bind(R.id.matrixItemAutocompleteName) + val subNameView by bind(R.id.matrixItemAutocompleteSubname) + val avatarImageView by bind(R.id.matrixItemAutocompleteAvatar) } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt new file mode 100644 index 0000000000..51285b02b7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.room + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.autocompleteMatrixItem +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class AutocompleteRoomController @Inject constructor() : TypedEpoxyController>() { + + var listener: AutocompleteClickListener? = null + + @Inject lateinit var avatarRenderer: AvatarRenderer + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { roomSummary -> + autocompleteMatrixItem { + id(roomSummary.roomId) + matrixItem(roomSummary.toMatrixItem()) + subName(roomSummary.canonicalAlias) + avatarRenderer(avatarRenderer) + clickListener { _ -> + listener?.onItemClick(roomSummary) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt new file mode 100644 index 0000000000..e2b4f38e19 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.room + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Success +import com.otaliastudios.autocomplete.RecyclerViewPresenter +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import javax.inject.Inject + +class AutocompleteRoomPresenter @Inject constructor(context: Context, + private val controller: AutocompleteRoomController +) : RecyclerViewPresenter(context), AutocompleteClickListener { + + var callback: Callback? = null + + init { + controller.listener = this + } + + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + return controller.adapter + } + + override fun onItemClick(t: RoomSummary) { + dispatchClick(t) + } + + override fun onQuery(query: CharSequence?) { + callback?.onQueryRooms(query) + } + + fun render(rooms: Async>) { + if (rooms is Success) { + controller.setData(rooms()) + } + } + + interface Callback { + fun onQueryRooms(query: CharSequence?) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt index 8f0090001f..53a87fe27a 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt @@ -20,6 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.autocompleteMatrixItem import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject @@ -34,7 +35,7 @@ class AutocompleteUserController @Inject constructor() : TypedEpoxyController
  • - autocompleteUserItem { + autocompleteMatrixItem { id(user.userId) matrixItem(user.toMatrixItem()) avatarRenderer(avatarRenderer) 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 49f23f7f2c..0d20ce851e 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 @@ -60,6 +60,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -68,6 +69,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.api.util.toRoomAliasMatrixItem import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer @@ -83,6 +85,7 @@ import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy +import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer @@ -140,6 +143,7 @@ class RoomDetailFragment @Inject constructor( private val commandAutocompletePolicy: CommandAutocompletePolicy, private val autocompleteCommandPresenter: AutocompleteCommandPresenter, private val autocompleteUserPresenter: AutocompleteUserPresenter, + private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, val roomDetailViewModelFactory: RoomDetailViewModel.Factory, @@ -150,6 +154,7 @@ class RoomDetailFragment @Inject constructor( VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback, + AutocompleteRoomPresenter.Callback, VectorInviteView.Callback, JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, @@ -582,6 +587,52 @@ class RoomDetailFragment @Inject constructor( }) .build() + autocompleteRoomPresenter.callback = this + Autocomplete.on(composerLayout.composerEditText) + .with(CharPolicy('#', true)) + .with(autocompleteRoomPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { + // Detect last '#' and remove it + var startIndex = editable.lastIndexOf("#") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val matrixItem = item.toRoomAliasMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + requireContext(), + matrixItem + ) + span.bind(composerLayout.composerEditText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + autocompleteUserPresenter.callback = this Autocomplete.on(composerLayout.composerEditText) .with(CharPolicy('@', true)) @@ -724,6 +775,7 @@ class RoomDetailFragment @Inject constructor( private fun renderTextComposerState(state: TextComposerViewState) { autocompleteUserPresenter.render(state.asyncUsers) + autocompleteRoomPresenter.render(state.asyncRooms) } private fun renderTombstoneEventHandling(async: Async) { @@ -1056,6 +1108,12 @@ class RoomDetailFragment @Inject constructor( textComposerViewModel.handle(TextComposerAction.QueryUsers(query)) } + // AutocompleteRoomPresenter.Callback + + override fun onQueryRooms(query: CharSequence?) { + textComposerViewModel.handle(TextComposerAction.QueryRooms(query)) + } + private fun handleActions(action: EventSharedAction) { when (action) { is EventSharedAction.AddReaction -> { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt index 5d60fa1cef..9f94b086a2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt @@ -20,4 +20,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class TextComposerAction : VectorViewModelAction { data class QueryUsers(val query: CharSequence?) : TextComposerAction() + data class QueryRooms(val query: CharSequence?) : TextComposerAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt index 88548e12b4..c69ab7c5da 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt @@ -24,6 +24,7 @@ import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.VectorViewModel @@ -32,16 +33,16 @@ import io.reactivex.Observable import io.reactivex.functions.BiFunction import java.util.concurrent.TimeUnit -typealias AutocompleteUserQuery = CharSequence +typealias AutocompleteQuery = CharSequence class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState, private val session: Session ) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId)!! - private val roomId = initialState.roomId - private val usersQueryObservable = BehaviorRelay.create>() + private val usersQueryObservable = BehaviorRelay.create>() + private val roomsQueryObservable = BehaviorRelay.create>() @AssistedInject.Factory interface Factory { @@ -59,11 +60,13 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: init { observeUsersQuery() + observeRoomsQuery() } override fun handle(action: TextComposerAction) { when (action) { is TextComposerAction.QueryUsers -> handleQueryUsers(action) + is TextComposerAction.QueryRooms -> handleQueryRooms(action) } } @@ -72,8 +75,14 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: usersQueryObservable.accept(query) } + + private fun handleQueryRooms(action: TextComposerAction.QueryRooms) { + val query = Option.fromNullable(action.query) + roomsQueryObservable.accept(query) + } + private fun observeUsersQuery() { - Observable.combineLatest, Option, List>( + Observable.combineLatest, Option, List>( room.rx().liveRoomMemberIds(), usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), BiFunction { roomMemberIds, query -> @@ -87,6 +96,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: it.displayName?.startsWith(prefix = filter, ignoreCase = true) ?: false } } + .sortedBy { it.displayName } } ).execute { async -> copy( @@ -94,4 +104,24 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: ) } } + + private fun observeRoomsQuery() { + Observable.combineLatest, Option, List>( + session.rx().liveRoomSummaries(), + roomsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), + BiFunction { roomSummaries, query -> + val filter = query.orNull() ?: "" + // Keep only room with a canonical alias + roomSummaries + .filter { + it.canonicalAlias?.contains(filter, ignoreCase = true) == true + } + .sortedBy { it.displayName } + } + ).execute { async -> + copy( + asyncRooms = async + ) + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt index b2cec09096..671a7ac556 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt @@ -19,11 +19,13 @@ package im.vector.riotx.features.home.room.detail.composer import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.features.home.room.detail.RoomDetailArgs data class TextComposerViewState(val roomId: String, - val asyncUsers: Async> = Uninitialized + val asyncUsers: Async> = Uninitialized, + val asyncRooms: Async> = Uninitialized ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/res/layout/item_autocomplete_matrix_item.xml b/vector/src/main/res/layout/item_autocomplete_matrix_item.xml new file mode 100644 index 0000000000..9696a08bb5 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_matrix_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_autocomplete_user.xml b/vector/src/main/res/layout/item_autocomplete_user.xml deleted file mode 100644 index f2fdb354a9..0000000000 --- a/vector/src/main/res/layout/item_autocomplete_user.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - \ No newline at end of file From c31b64771bfa1692b02b558eaea99667e4bd7da3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 00:42:19 +0100 Subject: [PATCH 022/105] Autocompletion: disable animation on the recycler view items --- .../autocomplete/command/AutocompleteCommandPresenter.kt | 2 ++ .../features/autocomplete/room/AutocompleteRoomPresenter.kt | 2 ++ .../features/autocomplete/user/AutocompleteUserPresenter.kt | 2 ++ 3 files changed, 6 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt index 2f076c4c53..84ae8db217 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -32,6 +32,8 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null return controller.adapter } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt index e2b4f38e19..53fed7f859 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt @@ -36,6 +36,8 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null return controller.adapter } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt index d60ea5db67..01dceb5399 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt @@ -36,6 +36,8 @@ class AutocompleteUserPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null return controller.adapter } From 05a788453f44b84461586842cbdfc498698240ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 00:47:47 +0100 Subject: [PATCH 023/105] More generic name --- .../api/session/room/model/relation/RelationService.kt | 2 +- .../room/send/{UserMentionSpan.kt => MatrixItemSpan.kt} | 4 ++-- .../matrix/android/api/session/room/send/SendService.kt | 2 +- .../internal/session/room/send/pills/MentionLinkSpec.kt | 4 ++-- .../internal/session/room/send/pills/TextPillsUtils.kt | 6 +++--- .../java/im/vector/riotx/features/html/PillImageSpan.kt | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/{UserMentionSpan.kt => MatrixItemSpan.kt} (90%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index b3dd1c6f22..7d8f2f0bc1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -98,7 +98,7 @@ interface RelationService { /** * Reply to an event in the timeline (must be in same room) * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 - * The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated + * The replyText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated * by the sdk into pills. * @param eventReplied the event referenced by the reply * @param replyText the reply text diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/MatrixItemSpan.kt similarity index 90% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/MatrixItemSpan.kt index 71a422bac8..d191f5197b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/MatrixItemSpan.kt @@ -19,9 +19,9 @@ package im.vector.matrix.android.api.session.room.send import im.vector.matrix.android.api.util.MatrixItem /** - * Tag class for spans that should mention a user. + * Tag class for spans that should mention a matrix item. * These Spans will be transformed into pills when detected in message to send */ -interface UserMentionSpan { +interface MatrixItemSpan { val matrixItem: MatrixItem } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index bdae5eaaa6..ac1b50bbcb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -29,7 +29,7 @@ interface SendService { /** * Method to send a text message asynchronously. - * The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated + * The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated * by the sdk into pills. * @param text the text message to send * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt index 5ad61b5441..055eade5e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt @@ -16,10 +16,10 @@ package im.vector.matrix.android.internal.session.room.send.pills -import im.vector.matrix.android.api.session.room.send.UserMentionSpan +import im.vector.matrix.android.api.session.room.send.MatrixItemSpan internal data class MentionLinkSpec( - val span: UserMentionSpan, + val span: MatrixItemSpan, val start: Int, val end: Int ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index c079e456c0..b512f27602 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.room.send.pills import android.text.SpannableString -import im.vector.matrix.android.api.session.room.send.UserMentionSpan +import im.vector.matrix.android.api.session.room.send.MatrixItemSpan import java.util.* import javax.inject.Inject @@ -49,7 +49,7 @@ internal class TextPillsUtils @Inject constructor( private fun transformPills(text: CharSequence, template: String): String? { val spannableString = SpannableString.valueOf(text) val pills = spannableString - ?.getSpans(0, text.length, UserMentionSpan::class.java) + ?.getSpans(0, text.length, MatrixItemSpan::class.java) ?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) } ?.toMutableList() ?.takeIf { it.isNotEmpty() } @@ -65,7 +65,7 @@ internal class TextPillsUtils @Inject constructor( // append text before pill append(text, currIndex, start) // append the pill - append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName)) + append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.getBestName())) currIndex = end } // append text after the last pill diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt index 8b57006439..a609541a62 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt @@ -28,7 +28,7 @@ import androidx.annotation.UiThread import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.google.android.material.chip.ChipDrawable -import im.vector.matrix.android.api.session.room.send.UserMentionSpan +import im.vector.matrix.android.api.session.room.send.MatrixItemSpan import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.glide.GlideRequests @@ -38,13 +38,13 @@ import java.lang.ref.WeakReference /** * This span is able to replace a text by a [ChipDrawable] * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached. - * Implements UserMentionSpan so that it could be automatically transformed in matrix links and displayed as pills. + * Implements MatrixItemSpan so that it could be automatically transformed in matrix links and displayed as pills. */ class PillImageSpan(private val glideRequests: GlideRequests, private val avatarRenderer: AvatarRenderer, private val context: Context, override val matrixItem: MatrixItem -) : ReplacementSpan(), UserMentionSpan { +) : ReplacementSpan(), MatrixItemSpan { private val pillDrawable = createChipDrawable() private val target = PillImageSpanTarget(this) From 543c07fd695adc295168618d4784011caa09b7da Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 01:23:45 +0100 Subject: [PATCH 024/105] Render pills for room links --- .../android/api/session/room/RoomService.kt | 7 +++ .../session/room/DefaultRoomService.kt | 17 ++++++ .../riotx/features/html/MxLinkTagHandler.kt | 53 ++++++++++++------- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index afe7cf8bc3..ba3b5ded78 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -52,6 +52,13 @@ interface RoomService { */ fun getRoom(roomId: String): Room? + /** + * Get a roomSummary from a roomId or a room alias + * @param roomIdOrAlias the roomId or the alias of a room to look for. + * @return a matching room summary or null + */ + fun getRoomSummary(roomIdOrAlias: String): RoomSummary? + /** * Get a live list of room summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of [RoomSummary] diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index de60e6e7e4..b53fa3ce33 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields +import im.vector.matrix.android.internal.database.query.findByAlias import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask @@ -38,6 +39,7 @@ import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.fetchCopyMap import io.realm.Realm import javax.inject.Inject @@ -69,6 +71,21 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona } } + override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { + return monarchy + .fetchCopyMap({ + if (roomIdOrAlias.startsWith("!")) { + // It's a roomId + RoomSummaryEntity.where(it, roomId = roomIdOrAlias).findFirst() + } else { + // Assume it's a room alias + RoomSummaryEntity.findByAlias(it, roomIdOrAlias) + } + }, { entity, _ -> + roomSummaryMapper.map(entity) + }) + } + override fun liveRoomSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt index 3f16666221..7e5a82060a 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -20,6 +20,7 @@ import android.content.Context import android.text.style.URLSpan import im.vector.matrix.android.api.permalinks.PermalinkData import im.vector.matrix.android.api.permalinks.PermalinkParser +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideRequests @@ -39,26 +40,42 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, val link = tag.attributes()["href"] if (link != null) { val permalinkData = PermalinkParser.parse(link) - when (permalinkData) { - is PermalinkData.UserLink -> { + val matrixItem = when (permalinkData) { + is PermalinkData.UserLink -> { val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId) - val span = PillImageSpan(glideRequests, avatarRenderer, context, MatrixItem.UserItem(permalinkData.userId, user?.displayName - ?: permalinkData.userId, user?.avatarUrl)) - SpannableBuilder.setSpans( - visitor.builder(), - span, - tag.start(), - tag.end() - ) - // also add clickable span - SpannableBuilder.setSpans( - visitor.builder(), - URLSpan(link), - tag.start(), - tag.end() - ) + MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl) } - else -> super.handle(visitor, renderer, tag) + is PermalinkData.RoomLink -> { + val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) + if (permalinkData.isRoomAlias) { + MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + } else { + MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + } + } + is PermalinkData.GroupLink -> { + // TODO val group = sessionHolder.getSafeActiveSession()?.getGroup(permalinkData.groupId) + MatrixItem.RoomItem(permalinkData.groupId /* TODO Group display name and avatar */) + } + else -> null + } + + if (matrixItem == null) { + super.handle(visitor, renderer, tag) + } else { + val span = PillImageSpan(glideRequests, avatarRenderer, context, matrixItem) + SpannableBuilder.setSpans( + visitor.builder(), + span, + tag.start(), + tag.end() + ) + SpannableBuilder.setSpans( + visitor.builder(), + URLSpan(link), + tag.start(), + tag.end() + ) } } else { super.handle(visitor, renderer, tag) From 8dce98c53827187d1bc13542b1609c652e50e2a1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 02:15:48 +0100 Subject: [PATCH 025/105] Autocompletion: group (including pills for groups) --- .../android/api/session/group/GroupService.kt | 7 +++ .../matrix/android/api/util/MatrixItem.kt | 3 + .../session/group/DefaultGroupService.kt | 8 +++ .../session/room/send/pills/TextPillsUtils.kt | 2 - .../group/AutocompleteGroupPresenter.kt | 61 +++++++++++++++++++ .../group/AutocompleteRoomController.kt | 48 +++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 57 +++++++++++++++++ .../detail/composer/TextComposerAction.kt | 1 + .../detail/composer/TextComposerViewModel.kt | 32 +++++++++- .../detail/composer/TextComposerViewState.kt | 4 +- .../riotx/features/html/MxLinkTagHandler.kt | 4 +- 11 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt index ff63d1a9e7..2d55d0be57 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt @@ -31,6 +31,13 @@ interface GroupService { */ fun getGroup(groupId: String): Group? + /** + * Get a groupSummary from a groupId + * @param groupId the groupId to look for. + * @return the groupSummary with groupId or null + */ + fun getGroupSummary(groupId: String): GroupSummary? + /** * Get a live list of group summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of [GroupSummary] diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index 1ca7237d3d..4c8082b77e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt @@ -74,6 +74,9 @@ sealed class MatrixItem( init { if (BuildConfig.DEBUG) checkId() } + + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + override fun getBestName() = id } open fun getBestName(): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt index be059038f3..192c6fe40c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.util.fetchCopyMap import javax.inject.Inject internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService { @@ -33,6 +34,13 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon return null } + override fun getGroupSummary(groupId: String): GroupSummary? { + return monarchy.fetchCopyMap( + { realm -> GroupSummaryEntity.where(realm, groupId).findFirst() }, + { it, _ -> it.asDomain() } + ) + } + override fun liveGroupSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) }, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index b512f27602..1a7b8228b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt @@ -23,8 +23,6 @@ import javax.inject.Inject /** * Utility class to detect special span in CharSequence and turn them into * formatted text to send them as a Matrix messages. - * - * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) */ internal class TextPillsUtils @Inject constructor( private val mentionLinkSpecComparator: MentionLinkSpecComparator diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt new file mode 100644 index 0000000000..822ce451e7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.group + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Success +import com.otaliastudios.autocomplete.RecyclerViewPresenter +import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import javax.inject.Inject + +class AutocompleteGroupPresenter @Inject constructor(context: Context, + private val controller: AutocompleteGroupController +) : RecyclerViewPresenter(context), AutocompleteClickListener { + + var callback: Callback? = null + + init { + controller.listener = this + } + + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null + return controller.adapter + } + + override fun onItemClick(t: GroupSummary) { + dispatchClick(t) + } + + override fun onQuery(query: CharSequence?) { + callback?.onQueryGroups(query) + } + + fun render(groups: Async>) { + if (groups is Success) { + controller.setData(groups()) + } + } + + interface Callback { + fun onQueryGroups(query: CharSequence?) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt new file mode 100644 index 0000000000..5d0d43d9ea --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.group + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.autocompleteMatrixItem +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class AutocompleteGroupController @Inject constructor() : TypedEpoxyController>() { + + var listener: AutocompleteClickListener? = null + + @Inject lateinit var avatarRenderer: AvatarRenderer + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { groupSummary -> + autocompleteMatrixItem { + id(groupSummary.groupId) + matrixItem(groupSummary.toMatrixItem()) + avatarRenderer(avatarRenderer) + clickListener { _ -> + listener?.onItemClick(groupSummary) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 0d20ce851e..cb54aba651 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 @@ -59,6 +59,7 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.* @@ -85,6 +86,7 @@ import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy +import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.command.Command @@ -144,6 +146,7 @@ class RoomDetailFragment @Inject constructor( private val autocompleteCommandPresenter: AutocompleteCommandPresenter, private val autocompleteUserPresenter: AutocompleteUserPresenter, private val autocompleteRoomPresenter: AutocompleteRoomPresenter, + private val autocompleteGroupPresenter: AutocompleteGroupPresenter, private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, val roomDetailViewModelFactory: RoomDetailViewModel.Factory, @@ -155,6 +158,7 @@ class RoomDetailFragment @Inject constructor( TimelineEventController.Callback, AutocompleteUserPresenter.Callback, AutocompleteRoomPresenter.Callback, + AutocompleteGroupPresenter.Callback, VectorInviteView.Callback, JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, @@ -633,6 +637,52 @@ class RoomDetailFragment @Inject constructor( }) .build() + autocompleteGroupPresenter.callback = this + Autocomplete.on(composerLayout.composerEditText) + .with(CharPolicy('+', true)) + .with(autocompleteGroupPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { + // Detect last '+' and remove it + var startIndex = editable.lastIndexOf("+") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val matrixItem = item.toMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + requireContext(), + matrixItem + ) + span.bind(composerLayout.composerEditText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + autocompleteUserPresenter.callback = this Autocomplete.on(composerLayout.composerEditText) .with(CharPolicy('@', true)) @@ -776,6 +826,7 @@ class RoomDetailFragment @Inject constructor( private fun renderTextComposerState(state: TextComposerViewState) { autocompleteUserPresenter.render(state.asyncUsers) autocompleteRoomPresenter.render(state.asyncRooms) + autocompleteGroupPresenter.render(state.asyncGroups) } private fun renderTombstoneEventHandling(async: Async) { @@ -1114,6 +1165,12 @@ class RoomDetailFragment @Inject constructor( textComposerViewModel.handle(TextComposerAction.QueryRooms(query)) } + // AutocompleteGroupPresenter.Callback + + override fun onQueryGroups(query: CharSequence?) { + textComposerViewModel.handle(TextComposerAction.QueryGroups(query)) + } + private fun handleActions(action: EventSharedAction) { when (action) { is EventSharedAction.AddReaction -> { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt index 9f94b086a2..0f5bf2a8c5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt @@ -21,4 +21,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class TextComposerAction : VectorViewModelAction { data class QueryUsers(val query: CharSequence?) : TextComposerAction() data class QueryRooms(val query: CharSequence?) : TextComposerAction() + data class QueryGroups(val query: CharSequence?) : TextComposerAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt index c69ab7c5da..9d6d9aab2e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt @@ -24,6 +24,7 @@ import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.rx.rx @@ -43,6 +44,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: private val usersQueryObservable = BehaviorRelay.create>() private val roomsQueryObservable = BehaviorRelay.create>() + private val groupsQueryObservable = BehaviorRelay.create>() @AssistedInject.Factory interface Factory { @@ -61,12 +63,14 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: init { observeUsersQuery() observeRoomsQuery() + observeGroupsQuery() } override fun handle(action: TextComposerAction) { when (action) { - is TextComposerAction.QueryUsers -> handleQueryUsers(action) - is TextComposerAction.QueryRooms -> handleQueryRooms(action) + is TextComposerAction.QueryUsers -> handleQueryUsers(action) + is TextComposerAction.QueryRooms -> handleQueryRooms(action) + is TextComposerAction.QueryGroups -> handleQueryGroups(action) } } @@ -81,6 +85,11 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: roomsQueryObservable.accept(query) } + private fun handleQueryGroups(action: TextComposerAction.QueryGroups) { + val query = Option.fromNullable(action.query) + groupsQueryObservable.accept(query) + } + private fun observeUsersQuery() { Observable.combineLatest, Option, List>( room.rx().liveRoomMemberIds(), @@ -124,4 +133,23 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: ) } } + + private fun observeGroupsQuery() { + Observable.combineLatest, Option, List>( + session.rx().liveGroupSummaries(), + groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), + BiFunction { groupSummaries, query -> + val filter = query.orNull() ?: "" + groupSummaries + .filter { + it.groupId.contains(filter, ignoreCase = true) + } + .sortedBy { it.displayName } + } + ).execute { async -> + copy( + asyncGroups = async + ) + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt index 671a7ac556..e863970afe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt @@ -19,13 +19,15 @@ package im.vector.riotx.features.home.room.detail.composer import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.features.home.room.detail.RoomDetailArgs data class TextComposerViewState(val roomId: String, val asyncUsers: Async> = Uninitialized, - val asyncRooms: Async> = Uninitialized + val asyncRooms: Async> = Uninitialized, + val asyncGroups: Async> = Uninitialized ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt index 7e5a82060a..ff85a4d2cb 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -54,8 +54,8 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, } } is PermalinkData.GroupLink -> { - // TODO val group = sessionHolder.getSafeActiveSession()?.getGroup(permalinkData.groupId) - MatrixItem.RoomItem(permalinkData.groupId /* TODO Group display name and avatar */) + val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId) + MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl) } else -> null } From c79b35b089bb8018acc2db569056bdbfe1bd9986 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:01:06 +0100 Subject: [PATCH 026/105] Autocomplete item layout --- vector/src/main/res/layout/item_autocomplete_command.xml | 9 ++++++--- .../main/res/layout/item_autocomplete_matrix_item.xml | 7 ++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/vector/src/main/res/layout/item_autocomplete_command.xml b/vector/src/main/res/layout/item_autocomplete_command.xml index 4fe815fbed..ce2cd6c731 100644 --- a/vector/src/main/res/layout/item_autocomplete_command.xml +++ b/vector/src/main/res/layout/item_autocomplete_command.xml @@ -1,11 +1,14 @@ - + android:foreground="?attr/selectableItemBackground" + android:paddingStart="8dp" + android:paddingTop="6dp" + android:paddingEnd="8dp" + android:paddingBottom="6dp"> @@ -18,8 +19,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" - android:layout_marginStart="12dp" - android:layout_marginLeft="12dp" + android:layout_marginStart="8dp" + android:layout_marginLeft="8dp" android:orientation="vertical"> Date: Fri, 20 Dec 2019 10:15:53 +0100 Subject: [PATCH 027/105] Remove extra blank line --- vector/src/main/res/layout/bottom_sheet_generic_list.xml | 1 - vector/src/main/res/layout/fragment_public_rooms.xml | 1 - vector/src/main/res/layout/fragment_room_directory_picker.xml | 1 - .../src/main/res/layout/item_timeline_event_code_block_stub.xml | 1 - .../main/res/layout/item_timeline_event_merged_header_stub.xml | 1 - vector/src/main/res/layout/vector_message_merge_avatar_list.xml | 1 - .../src/main/res/layout/vector_settings_address_preference.xml | 1 - .../res/layout/vector_settings_list_preference_with_warning.xml | 1 - vector/src/main/res/layout/vector_settings_round_avatar.xml | 1 - .../src/main/res/layout/vector_settings_spinner_preference.xml | 1 - vector/src/main/res/layout/view_attachment_type_selector.xml | 1 - vector/src/main/res/layout/view_read_marker.xml | 1 - 12 files changed, 12 deletions(-) diff --git a/vector/src/main/res/layout/bottom_sheet_generic_list.xml b/vector/src/main/res/layout/bottom_sheet_generic_list.xml index 69b5ce2fac..0bd6665325 100644 --- a/vector/src/main/res/layout/bottom_sheet_generic_list.xml +++ b/vector/src/main/res/layout/bottom_sheet_generic_list.xml @@ -1,5 +1,4 @@ - - - - - - - diff --git a/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml b/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml index 137d96408d..548594fcd4 100644 --- a/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml +++ b/vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml @@ -1,5 +1,4 @@ - diff --git a/vector/src/main/res/layout/vector_settings_round_avatar.xml b/vector/src/main/res/layout/vector_settings_round_avatar.xml index 66c2d2412b..fba69dc588 100644 --- a/vector/src/main/res/layout/vector_settings_round_avatar.xml +++ b/vector/src/main/res/layout/vector_settings_round_avatar.xml @@ -1,5 +1,4 @@ - - diff --git a/vector/src/main/res/layout/view_attachment_type_selector.xml b/vector/src/main/res/layout/view_attachment_type_selector.xml index f713561084..134ad47c92 100644 --- a/vector/src/main/res/layout/view_attachment_type_selector.xml +++ b/vector/src/main/res/layout/view_attachment_type_selector.xml @@ -1,5 +1,4 @@ - - Date: Fri, 20 Dec 2019 10:16:17 +0100 Subject: [PATCH 028/105] Update changes.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 89c1942f39..ac4b27596e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Render aliases and canonical alias change in the timeline + - Fix autocompletion issues and add support for rooms and groups Other changes: - From c992d32afdfaf9c66d8815e2f2e264480eff1274 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:23:45 +0100 Subject: [PATCH 029/105] Improve algo --- .../detail/composer/TextComposerViewModel.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt index 9d6d9aab2e..538d13bb4e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt @@ -102,7 +102,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: users } else { users.filter { - it.displayName?.startsWith(prefix = filter, ignoreCase = true) ?: false + it.displayName?.contains(filter, ignoreCase = true) ?: false } } .sortedBy { it.displayName } @@ -139,11 +139,15 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: session.rx().liveGroupSummaries(), groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), BiFunction { groupSummaries, query -> - val filter = query.orNull() ?: "" - groupSummaries - .filter { - it.groupId.contains(filter, ignoreCase = true) - } + val filter = query.orNull() + if (filter.isNullOrBlank()) { + groupSummaries + } else { + groupSummaries + .filter { + it.groupId.contains(filter, ignoreCase = true) + } + } .sortedBy { it.displayName } } ).execute { async -> From 3b0624ea401d6a4b1878804f78dd0f7a8298ca9b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:54:48 +0100 Subject: [PATCH 030/105] Fix issue with "in reply to" link --- .../vector/riotx/features/html/MxLinkTagHandler.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt index ff85a4d2cb..1167427a04 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -46,11 +46,16 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl) } is PermalinkData.RoomLink -> { - val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) - if (permalinkData.isRoomAlias) { - MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + // Exclude event link (used in reply event) + if (permalinkData.eventId == null) { + val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) + if (permalinkData.isRoomAlias) { + MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + } else { + MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + } } else { - MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + null } } is PermalinkData.GroupLink -> { From 3ee5a7f54d48af4a23d5b7fedd8b69e67b8086b2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:55:08 +0100 Subject: [PATCH 031/105] Better code --- .../android/api/permalinks/PermalinkParser.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt index d10152f4fe..871c30e46a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt @@ -56,23 +56,23 @@ object PermalinkParser { val identifier = params.getOrNull(0) val extraParameter = params.getOrNull(1) - if (identifier.isNullOrEmpty()) { - return PermalinkData.FallbackLink(uri) - } return when { + identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isRoomId(identifier) -> { - val eventId = extraParameter.takeIf { - !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) - } - PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId) + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = false, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) } MatrixPatterns.isRoomAlias(identifier) -> { - val eventId = extraParameter.takeIf { - !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) - } - PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId) + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = true, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) } else -> PermalinkData.FallbackLink(uri) } From 54f2ac0d8cea552e71b4691a0089c37e9772d9b6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 10:59:41 +0100 Subject: [PATCH 032/105] Better comment --- .../main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt index 1167427a04..76f7dfaabd 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -46,7 +46,6 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl) } is PermalinkData.RoomLink -> { - // Exclude event link (used in reply event) if (permalinkData.eventId == null) { val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) if (permalinkData.isRoomAlias) { @@ -55,6 +54,7 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) } } else { + // Exclude event link (used in reply events, we do not want to pill the "in reply to") null } } From 0ccb975d43dd4bab43140d86d329f4385bc976e7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 11:04:06 +0100 Subject: [PATCH 033/105] Disable MatrixLinkify --- .../matrix/android/api/permalinks/MatrixLinkify.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt index fc02cf4a61..cd4ce1206e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.api.permalinks import android.text.Spannable -import im.vector.matrix.android.api.MatrixPatterns /** * MatrixLinkify take a piece of text and turns all of the @@ -30,7 +29,13 @@ object MatrixLinkify { * * @param spannable the text in which the matrix items has to be clickable. */ + @Suppress("UNUSED_PARAMETER") fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean { + /** + * I disable it because it mess up with pills, and even with pills, it does not work correctly: + * The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to + */ + /* // sanity checks if (spannable.isEmpty()) { return false @@ -50,5 +55,7 @@ object MatrixLinkify { } } return hasMatch + */ + return false } } From 3cc65b1e71e7a84e569b6978395f5ad43a4cf444 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 11:05:54 +0100 Subject: [PATCH 034/105] ktlint --- ...ocompleteRoomController.kt => AutocompleteGroupController.kt} | 0 .../features/home/room/detail/composer/TextComposerViewModel.kt | 1 - 2 files changed, 1 deletion(-) rename vector/src/main/java/im/vector/riotx/features/autocomplete/group/{AutocompleteRoomController.kt => AutocompleteGroupController.kt} (100%) diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt similarity index 100% rename from vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteRoomController.kt rename to vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupController.kt diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt index 538d13bb4e..f7ec78c6c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt @@ -79,7 +79,6 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: usersQueryObservable.accept(query) } - private fun handleQueryRooms(action: TextComposerAction.QueryRooms) { val query = Option.fromNullable(action.query) roomsQueryObservable.accept(query) From e13281dc9750c7c8f95c42578b52e2d436059df3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Dec 2019 11:27:26 +0100 Subject: [PATCH 035/105] Update CHANGES and clean code --- CHANGES.md | 5 +++-- .../vector/riotx/features/home/room/list/RoomListFragment.kt | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 863b1ca455..10929998ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,13 +5,14 @@ Features ✨: - Improvements 🙌: - - + - The initial sync is now handled by a foreground service Other changes: - Bugfix 🐛: - - + - Fix avatar image disappearing (#777) + - Fix read marker banner when permalink Translations 🗣: - diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 27fbc387b1..e272c1423f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -28,7 +28,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.mvrx.* -import com.google.android.material.snackbar.Snackbar import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary From 83126d5f5585cf1ea63ad7ce58cd25adf2ab2246 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Dec 2019 11:54:59 +0100 Subject: [PATCH 036/105] Fix android tests not working --- .../SingleThreadCoroutineDispatcher.kt | 2 +- .../android/auth/AuthenticationServiceTest.kt | 60 ------------------- .../internal/crypto/CryptoStoreTest.kt | 16 ++++- .../session/room/timeline/ChunkEntityTest.kt | 8 ++- 4 files changed, 23 insertions(+), 63 deletions(-) delete mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticationServiceTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt index e63123f3b3..f2fbde3fe7 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt @@ -19,4 +19,4 @@ package im.vector.matrix.android import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.Dispatchers.Main -internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main) +internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticationServiceTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticationServiceTest.kt deleted file mode 100644 index c3babd7e5a..0000000000 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticationServiceTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.auth - -import androidx.test.annotation.UiThreadTest -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.GrantPermissionRule -import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.OkReplayRuleChainNoActivity -import im.vector.matrix.android.api.auth.AuthenticationService -import okreplay.* -import org.junit.ClassRule -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -internal class AuthenticationServiceTest : InstrumentedTest { - - lateinit var authenticationService: AuthenticationService - lateinit var okReplayInterceptor: OkReplayInterceptor - - private val okReplayConfig = OkReplayConfig.Builder() - .tapeRoot(AndroidTapeRoot( - context(), javaClass)) - .defaultMode(TapeMode.READ_WRITE) // or TapeMode.READ_ONLY - .sslEnabled(true) - .interceptor(okReplayInterceptor) - .build() - - @get:Rule - val testRule = OkReplayRuleChainNoActivity(okReplayConfig).get() - - @Test - @UiThreadTest - @OkReplay(tape = "auth", mode = TapeMode.READ_WRITE) - fun auth() { - } - - companion object { - @ClassRule - @JvmField - val grantExternalStoragePermissionRule: GrantPermissionRule = - GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - } -} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt index 3fc3079cc7..bab0a366ce 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt @@ -16,20 +16,34 @@ package im.vector.matrix.android.internal.crypto +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import io.realm.Realm +import io.realm.RealmConfiguration import org.junit.Assert.* +import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.matrix.olm.OlmAccount import org.matrix.olm.OlmManager import org.matrix.olm.OlmSession private const val DUMMY_DEVICE_KEY = "DeviceKey" -class CryptoStoreTest { +@RunWith(AndroidJUnit4::class) +class CryptoStoreTest : InstrumentedTest { private val cryptoStoreHelper = CryptoStoreHelper() + @Before + fun setup() { + Realm.init(context()) + } + + @Test fun test_metadata_realm_ok() { val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt index abb990c979..9c81a8e18f 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -19,8 +19,10 @@ package im.vector.matrix.android.session.room.timeline import androidx.test.ext.junit.runners.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.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 @@ -43,7 +45,11 @@ internal class ChunkEntityTest : InstrumentedTest { @Before fun setup() { Realm.init(context()) - val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build() + val testConfig = RealmConfiguration.Builder() + .inMemory() + .name("test-realm") + .modules(SessionRealmModule()) + .build() monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build() } From c54358831faece6549ae51b95d7dd48544e5d360 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 17:45:32 +0100 Subject: [PATCH 037/105] Group throwable extension together --- .../matrix/android/api/failure}/Extensions.kt | 16 +++++++++------- .../vector/matrix/android/api/failure/Failure.kt | 2 -- .../login/LoginGenericTextInputFormFragment.kt | 2 +- ...LoginResetPasswordMailConfirmationFragment.kt | 2 +- .../features/login/LoginWaitForEmailFragment.kt | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) rename {vector/src/main/java/im/vector/riotx/core/error => matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure}/Extensions.kt (60%) diff --git a/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt similarity index 60% rename from vector/src/main/java/im/vector/riotx/core/error/Extensions.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt index 614340bd3d..a3b5ce39eb 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt @@ -14,13 +14,15 @@ * limitations under the License. */ -package im.vector.riotx.core.error +package im.vector.matrix.android.api.failure -import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.failure.MatrixError import javax.net.ssl.HttpsURLConnection -fun Throwable.is401(): Boolean { - return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ - && error.code == MatrixError.M_UNAUTHORIZED) -} +fun Throwable.is401() = + this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && error.code == MatrixError.M_UNAUTHORIZED + +fun Throwable.isTokenError() = + this is Failure.ServerError + && (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt index 611b2fb7d3..4d44e3346b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt @@ -44,5 +44,3 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { abstract class FeatureFailure : Failure() } - -fun Throwable.isTokenError() = this is Failure.ServerError && (this.error.code == MatrixError.M_UNKNOWN_TOKEN || this.error.code == MatrixError.M_MISSING_TOKEN) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 64fb01fa5f..3ee1cd6d64 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -30,8 +30,8 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.auth.registration.RegisterThreePid import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.is401 import im.vector.riotx.R -import im.vector.riotx.core.error.is401 import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.isEmail import im.vector.riotx.core.extensions.setTextOrHide diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt index e7ddc78853..cace48b7f2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt @@ -20,8 +20,8 @@ import androidx.appcompat.app.AlertDialog import butterknife.OnClick import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Success +import im.vector.matrix.android.api.failure.is401 import im.vector.riotx.R -import im.vector.riotx.core.error.is401 import kotlinx.android.synthetic.main.fragment_login_reset_password_mail_confirmation.* import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt index 8a12c67106..c4507876d7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt @@ -20,8 +20,8 @@ import android.os.Bundle import android.os.Parcelable import android.view.View import com.airbnb.mvrx.args +import im.vector.matrix.android.api.failure.is401 import im.vector.riotx.R -import im.vector.riotx.core.error.is401 import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_login_wait_for_email.* import javax.inject.Inject From 4fe9c52737df92da4db13c79c2a8f45f7d37e4ea Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Dec 2019 17:54:02 +0100 Subject: [PATCH 038/105] Move permission to the main AndroidManifest --- vector/src/fdroid/AndroidManifest.xml | 1 - vector/src/gplay/AndroidManifest.xml | 2 -- vector/src/main/AndroidManifest.xml | 1 + 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml index 51234e0bba..32cdba9251 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector/src/fdroid/AndroidManifest.xml @@ -4,7 +4,6 @@ - diff --git a/vector/src/gplay/AndroidManifest.xml b/vector/src/gplay/AndroidManifest.xml index 85567df2c1..db30f21f7a 100755 --- a/vector/src/gplay/AndroidManifest.xml +++ b/vector/src/gplay/AndroidManifest.xml @@ -2,8 +2,6 @@ - - diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e97e7d767d..124763916b 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + Date: Fri, 20 Dec 2019 17:54:35 +0100 Subject: [PATCH 039/105] ktlint --- .../vector/matrix/android/internal/crypto/CryptoStoreTest.kt | 3 --- .../matrix/android/session/room/timeline/ChunkEntityTest.kt | 1 - 2 files changed, 4 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt index bab0a366ce..df503f2486 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/CryptoStoreTest.kt @@ -17,12 +17,10 @@ package im.vector.matrix.android.internal.crypto import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import io.realm.Realm -import io.realm.RealmConfiguration import org.junit.Assert.* import org.junit.Before import org.junit.Test @@ -43,7 +41,6 @@ class CryptoStoreTest : InstrumentedTest { Realm.init(context()) } - @Test fun test_metadata_realm_ok() { val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt index 9c81a8e18f..592086b0ec 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.session.room.timeline import androidx.test.ext.junit.runners.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.SessionRealmModule From 00f316ba5d61a95be825891c385de877c1f2af2f Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Dec 2019 19:51:03 +0100 Subject: [PATCH 040/105] Room members: introduce RoomMemberEntity to be able to query. Still work to do. --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 8 +-- .../session/room/members/MembershipService.kt | 7 +- .../api/session/room/model/Membership.kt | 9 +++ .../api/session/room/model/RoomMember.kt | 23 ++----- .../session/room/model/RoomMemberContent.kt | 38 +++++++++++ .../matrix/android/api/util/MatrixItem.kt | 2 + .../database/helper/RoomEntityHelper.kt | 3 +- .../helper/TimelineEventEntityHelper.kt | 5 +- .../database/mapper/RoomMemberMapper.kt | 36 +++++++++++ .../database/model/RoomMemberEntity.kt | 43 +++++++++++++ .../database/model/RoomSummaryEntity.kt | 1 + .../database/model/SessionRealmModule.kt | 3 +- .../database/query/RoomMemberEntityQueries.kt | 36 +++++++++++ .../session/room/RoomAvatarResolver.kt | 12 ++-- .../session/room/RoomSummaryUpdater.kt | 6 +- .../membership/DefaultMembershipService.kt | 18 +++--- .../room/membership/LoadRoomMembersTask.kt | 7 +- .../membership/RoomDisplayNameResolver.kt | 49 +++++++------- .../membership/RoomMemberEntityFactory.kt | 36 +++++++++++ .../room/membership/RoomMemberEventHandler.kt | 47 ++++++++++++++ .../session/room/membership/RoomMembers.kt | 64 +++++++------------ .../room/timeline/TokenChunkEventPersistor.kt | 7 +- .../internal/session/sync/RoomSyncHandler.kt | 12 ++-- .../sync/UserAccountDataSyncHandler.kt | 5 +- .../session/user/UserEntityFactory.kt | 23 ++----- .../AutocompleteMemberController.kt} | 10 +-- .../AutocompleteMemberPresenter.kt} | 22 +++---- .../home/room/detail/RoomDetailFragment.kt | 23 +++---- .../detail/composer/TextComposerViewModel.kt | 16 ++--- .../detail/composer/TextComposerViewState.kt | 3 +- .../timeline/format/NoticeEventFormatter.kt | 8 +-- .../notifications/NotifiableEventResolver.kt | 3 +- 32 files changed, 393 insertions(+), 192 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt rename vector/src/main/java/im/vector/riotx/features/autocomplete/{user/AutocompleteUserController.kt => member/AutocompleteMemberController.kt} (80%) rename vector/src/main/java/im/vector/riotx/features/autocomplete/{user/AutocompleteUserPresenter.kt => member/AutocompleteMemberPresenter.kt} (66%) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index e5ebc536ff..bf4e924cf0 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -17,9 +17,7 @@ package im.vector.matrix.rx import im.vector.matrix.android.api.session.room.Room -import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary -import im.vector.matrix.android.api.session.room.model.ReadReceipt -import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.model.* import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -33,8 +31,8 @@ class RxRoom(private val room: Room) { return room.getRoomSummaryLive().asObservable() } - fun liveRoomMemberIds(): Observable> { - return room.getRoomMemberIdsLive().asObservable() + fun liveRoomMembers(memberships: List): Observable> { + return room.getRoomMembersLive(memberships).asObservable() } fun liveAnnotationSummary(eventId: String): Observable> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index 34af2cf572..12f0378af7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.members import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable @@ -41,11 +42,11 @@ interface MembershipService { fun getRoomMember(userId: String): RoomMember? /** - * Return all the roomMembers ids of the room - * + * Return all the roomMembers of the room filtered by memberships + * @param memberships list of accepted memberships * @return a [LiveData] of roomMember list. */ - fun getRoomMemberIdsLive(): LiveData> + fun getRoomMembersLive(memberships: List): LiveData> fun getNumberOfJoinedMembers(): Int diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt index 1894effc7a..4d35d3b4dd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt @@ -43,4 +43,13 @@ enum class Membership(val value: String) { fun isLeft(): Boolean { return this == KNOCK || this == LEAVE || this == BAN } + + companion object { + fun activeMemberships(): List { + return listOf(INVITE, JOIN) + } + } + } + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt index 6a4d8e3c94..994c27be4d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt @@ -16,23 +16,12 @@ package im.vector.matrix.android.api.session.room.model -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.session.events.model.UnsignedData - /** - * Class representing the EventType.STATE_ROOM_MEMBER state event content + * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content */ -@JsonClass(generateAdapter = true) data class RoomMember( - @Json(name = "membership") val membership: Membership, - @Json(name = "reason") val reason: String? = null, - @Json(name = "displayname") val displayName: String? = null, - @Json(name = "avatar_url") val avatarUrl: String? = null, - @Json(name = "is_direct") val isDirect: Boolean = false, - @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, - @Json(name = "unsigned") val unsignedData: UnsignedData? = null -) { - val safeReason - get() = reason?.takeIf { it.isNotBlank() } -} + val membership: Membership, + val userId: String, + val displayName: String? = null, + val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt new file mode 100644 index 0000000000..deeeb8ba52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.UnsignedData + +/** + * Class representing the EventType.STATE_ROOM_MEMBER state event content + */ +@JsonClass(generateAdapter = true) +data class RoomMemberContent( + @Json(name = "membership") val membership: Membership, + @Json(name = "reason") val reason: String? = null, + @Json(name = "displayname") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null, + @Json(name = "is_direct") val isDirect: Boolean = false, + @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, + @Json(name = "unsigned") val unsignedData: UnsignedData? = null +) { + val safeReason + get() = reason?.takeIf { it.isNotBlank() } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index 4c8082b77e..d6ef522f41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.util import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.user.model.User @@ -146,3 +147,4 @@ fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, ava fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl) +fun RoomMember.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index 948af2af96..19c4715faa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -60,7 +60,7 @@ internal fun RoomEntity.addSendingEvent(event: Event) { this.sendState = SendState.UNSENT } val roomMembers = RoomMembers(realm, roomId) - val myUser = roomMembers.get(senderId) + val myUser = roomMembers.getLastRoomMember(senderId) val localId = TimelineEventEntity.nextId(realm) val timelineEventEntity = TimelineEventEntity(localId).also { it.root = eventEntity @@ -69,7 +69,6 @@ internal fun RoomEntity.addSendingEvent(event: Event) { it.senderName = myUser?.displayName it.senderAvatar = myUser?.avatarUrl it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName) - it.senderMembershipEvent = roomMembers.queryRoomMemberEvent(senderId).findFirst() } sendingTimelineEvents.add(0, timelineEventEntity) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt index 36ed2f7edf..c5e2aef910 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.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 @@ -64,7 +65,7 @@ internal fun TimelineEventEntity.updateSenderData() { senderRoomMemberPrevContent = senderMembershipEvent?.prevContent } - ContentMapper.map(senderRoomMemberContent).toModel()?.also { + ContentMapper.map(senderRoomMemberContent).toModel()?.also { this.senderAvatar = it.avatarUrl this.senderName = it.displayName this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) @@ -72,7 +73,7 @@ internal fun TimelineEventEntity.updateSenderData() { // 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 { + ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also { if (this.senderAvatar == null && it.avatarUrl != null) { this.senderAvatar = it.avatarUrl } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt new file mode 100644 index 0000000000..a458c5e506 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.mapper + +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.internal.database.model.RoomMemberEntity + +internal object RoomMemberMapper { + + fun map(roomMemberEntity: RoomMemberEntity): RoomMember { + return RoomMember( + userId = roomMemberEntity.userId, + avatarUrl = roomMemberEntity.avatarUrl, + displayName = roomMemberEntity.displayName, + membership = roomMemberEntity.membership + ) + } +} + +internal fun RoomMemberEntity.asDomain(): RoomMember { + return RoomMemberMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt new file mode 100644 index 0000000000..c532857fe1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.model + +import im.vector.matrix.android.api.session.room.model.Membership +import io.realm.RealmObject +import io.realm.annotations.Index +import io.realm.annotations.PrimaryKey + +internal open class RoomMemberEntity(@PrimaryKey var primaryKey: String = "", + @Index var userId: String = "", + @Index var roomId: String = "", + var displayName: String = "", + var avatarUrl: String = "", + var reason: String? = null, + var isDirect: Boolean = false +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 406c8700b6..2fa892d874 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -42,6 +42,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, var canonicalAlias: String? = null, var aliases: RealmList = RealmList(), + // this is required for querying var flatAliases: String = "" ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 6059d3faf7..07ff1df005 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -49,6 +49,7 @@ import io.realm.annotations.RealmModule ReadMarkerEntity::class, UserDraftsEntity::class, DraftEntity::class, - HomeServerCapabilitiesEntity::class + HomeServerCapabilitiesEntity::class, + RoomMemberEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt new file mode 100644 index 0000000000..3eb1da87a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields +import im.vector.matrix.android.internal.database.model.UserEntity +import im.vector.matrix.android.internal.database.model.UserEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun RoomMemberEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery { + val query = realm + .where() + .equalTo(RoomMemberEntityFields.ROOM_ID, roomId) + + if (userId != null) { + query.equalTo(RoomMemberEntityFields.USER_ID, userId) + } + return query +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt index c9d5aeb6bb..acfb57bd46 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 @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId @@ -47,19 +48,16 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona return@doWithRealm } val roomMembers = RoomMembers(realm, roomId) - val members = roomMembers.queryRoomMembersEvent().findAll() + val members = roomMembers.queryActiveRoomMembersEvent().findAll() // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) if (members.size == 1) { - res = members.firstOrNull()?.toRoomMember()?.avatarUrl + res = members.firstOrNull()?.avatarUrl } else if (members.size == 2) { - val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, userId).findFirst() - res = firstOtherMember?.toRoomMember()?.avatarUrl + val firstOtherMember = members.where().notEqualTo(RoomMemberEntityFields.USER_ID, userId).findFirst() + res = firstOtherMember?.avatarUrl } } return res } - private fun EventEntity?.toRoomMember(): RoomMember? { - return ContentMapper.map(this?.content).toModel() - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index e9f33a547b..536484de5a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -24,8 +24,8 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.* @@ -113,10 +113,10 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId if (updateMembers) { val otherRoomMembers = RoomMembers(realm, roomId) .queryRoomMembersEvent() - .notEqualTo(EventEntityFields.STATE_KEY, userId) + .notEqualTo(RoomMemberEntityFields.USER_ID, userId) .findAll() .asSequence() - .map { it.stateKey } + .map { it.userId } roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 00c1c2c4ca..2b89f86d3e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -21,7 +21,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember @@ -33,6 +32,7 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRo import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopied +import io.realm.Realm internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String, private val monarchy: Monarchy, @@ -58,29 +58,27 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr } override fun getRoomMember(userId: String): RoomMember? { - val eventEntity = monarchy.fetchCopied { - RoomMembers(it, roomId).queryRoomMemberEvent(userId).findFirst() + val roomMemberEntity = monarchy.fetchCopied { + RoomMembers(it, roomId).getLastRoomMember(userId) } - return eventEntity?.asDomain()?.content.toModel() + return roomMemberEntity?.asDomain() } - override fun getRoomMemberIdsLive(): LiveData> { + override fun getRoomMembersLive(memberships: List): LiveData> { return monarchy.findAllMappedWithChanges( { RoomMembers(it, roomId).queryRoomMembersEvent() }, { - it.stateKey!! + it.asDomain() } ) } override fun getNumberOfJoinedMembers(): Int { - var result = 0 - monarchy.runTransactionSync { - result = RoomMembers(it, roomId).getNumberOfJoinedMembers() + return Realm.getInstance(monarchy.realmConfiguration).use { + RoomMembers(it, roomId).getNumberOfJoinedMembers() } - return result } override fun invite(userId: String, reason: String?, callback: MatrixCallback): Cancelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt index 7d9332ee84..6affd120a8 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 @@ -44,7 +44,8 @@ internal interface LoadRoomMembersTask : Task internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAPI: RoomAPI, private val monarchy: Monarchy, private val syncTokenStore: SyncTokenStore, - private val roomSummaryUpdater: RoomSummaryUpdater + private val roomSummaryUpdater: RoomSummaryUpdater, + private val roomMemberEventHandler: RoomMemberEventHandler ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { @@ -66,9 +67,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP for (roomMemberEvent in response.roomMemberEvents) { roomEntity.addStateEvent(roomMemberEvent) - UserEntityFactory.createOrNull(roomMemberEvent)?.also { - realm.insertOrUpdate(it) - } + roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) } roomEntity.chunks.flatMap { it.timelineEvents }.forEach { it.updateSenderData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 21270308ed..9382fbc54a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -23,9 +23,10 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.* import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.RoomMemberEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where @@ -75,43 +76,46 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } val roomMembers = RoomMembers(realm, roomId) - val loadedMembers = roomMembers.queryRoomMembersEvent().findAll() + val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll() if (roomEntity?.membership == Membership.INVITE) { - val inviteMeEvent = roomMembers.queryRoomMemberEvent(userId).findFirst() + val inviteMeEvent = roomMembers.getLastStateEvent(userId) val inviterId = inviteMeEvent?.sender name = if (inviterId != null) { - val inviterMemberEvent = loadedMembers.where() - .equalTo(EventEntityFields.STATE_KEY, inviterId) + activeMembers.where() + .equalTo(RoomMemberEntityFields.USER_ID, inviterId) .findFirst() - inviterMemberEvent?.toRoomMember()?.displayName + ?.displayName } else { context.getString(R.string.room_displayname_room_invite) } } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { - roomSummary.heroes.mapNotNull { - roomMembers.getStateEvent(it) + val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { + roomSummary.heroes.mapNotNull { userId -> + roomMembers.getLastRoomMember(userId)?.takeIf { + it.membership == Membership.INVITE || it.membership == Membership.JOIN + } } } else { - loadedMembers.where() - .notEqualTo(EventEntityFields.STATE_KEY, userId) + activeMembers.where() + .notEqualTo(RoomMemberEntityFields.USER_ID, userId) .limit(3) .findAll() + .createSnapshot() } - val otherMembersCount = roomMembers.getNumberOfMembers() - 1 + val otherMembersCount = otherMembersSubset.count() name = when (otherMembersCount) { 0 -> context.getString(R.string.room_displayname_empty_room) 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 2 -> context.getString(R.string.room_displayname_two_members, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - resolveRoomMemberName(otherMembersSubset[1], roomMembers) + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers) ) else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, - roomMembers.getNumberOfJoinedMembers() - 1, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - roomMembers.getNumberOfJoinedMembers() - 1) + roomMembers.getNumberOfJoinedMembers() - 1, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + roomMembers.getNumberOfJoinedMembers() - 1) } } return@doWithRealm @@ -119,19 +123,14 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } - private fun resolveRoomMemberName(eventEntity: EventEntity?, + private fun resolveRoomMemberName(roomMember: RoomMemberEntity?, roomMembers: RoomMembers): String? { - if (eventEntity == null) return null - val roomMember = eventEntity.toRoomMember() ?: return null + if (roomMember == null) return null val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName) return if (isUnique) { roomMember.displayName } else { - "${roomMember.displayName} (${eventEntity.stateKey})" + "${roomMember.displayName} (${roomMember.userId})" } } - - private fun EventEntity?.toRoomMember(): RoomMember? { - return ContentMapper.map(this?.content).toModel() - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt new file mode 100644 index 0000000000..933431b77f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.membership + +import im.vector.matrix.android.api.session.room.model.RoomMemberContent +import im.vector.matrix.android.internal.database.model.RoomMemberEntity + +internal object RoomMemberEntityFactory { + + fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberEntity { + val primaryKey = "${roomId}_${userId}" + return RoomMemberEntity( + primaryKey = primaryKey, + userId = userId, + roomId = roomId, + displayName = roomMember.displayName ?: "", + avatarUrl = roomMember.avatarUrl ?: "" + ).apply { + membership = roomMember.membership + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt new file mode 100644 index 0000000000..73c4c8cdc1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.membership + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberContent +import im.vector.matrix.android.internal.session.user.UserEntityFactory +import io.realm.Realm +import javax.inject.Inject + +internal class RoomMemberEventHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, event: Event): Boolean { + if (event.type != EventType.STATE_ROOM_MEMBER) { + return false + } + val roomMember = event.content.toModel() ?: return false + val userId = event.stateKey ?: return false + val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) + realm.insertOrUpdate(roomMemberEntity) + if (roomMember.membership == Membership.JOIN || roomMember.membership == Membership.INVITE) { + val userEntity = UserEntityFactory.create(userId, roomMember) + realm.insertOrUpdate(userEntity) + } + return true + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index 9fba1d8f02..ca397a0608 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -21,8 +21,9 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomMemberEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.where import io.realm.Realm @@ -42,19 +43,18 @@ internal class RoomMembers(private val realm: Realm, RoomSummaryEntity.where(realm, roomId).findFirst() } - fun getStateEvent(userId: String): EventEntity? { + fun getLastStateEvent(userId: String): EventEntity? { return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) - .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) .equalTo(EventEntityFields.STATE_KEY, userId) + .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) .findFirst() } - fun get(userId: String): RoomMember? { - return getStateEvent(userId) - ?.let { - it.asDomain().content?.toModel() - } + fun getLastRoomMember(userId: String): RoomMemberEntity? { + return RoomMemberEntity + .where(realm, roomId, userId) + .findFirst() } fun isUniqueDisplayName(displayName: String?): Boolean { @@ -69,36 +69,35 @@ internal class RoomMembers(private val realm: Realm, .size == 1 } - fun queryRoomMembersEvent(): RealmQuery { - return EventEntity - .where(realm, roomId, EventType.STATE_ROOM_MEMBER) - .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) - .isNotNull(EventEntityFields.STATE_KEY) - .distinct(EventEntityFields.STATE_KEY) - .isNotNull(EventEntityFields.CONTENT) + fun queryRoomMembersEvent(): RealmQuery { + return RoomMemberEntity.where(realm, roomId) } - fun queryJoinedRoomMembersEvent(): RealmQuery { - return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"join\"") + fun queryJoinedRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent().equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } - fun queryInvitedRoomMembersEvent(): RealmQuery { - return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"invite\"") + fun queryInvitedRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent().equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) } - fun queryRoomMemberEvent(userId: String): RealmQuery { + fun queryActiveRoomMembersEvent(): RealmQuery { return queryRoomMembersEvent() - .equalTo(EventEntityFields.STATE_KEY, userId) + .beginGroup() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + .or() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + .endGroup() } fun getNumberOfJoinedMembers(): Int { return roomSummary?.joinedMembersCount - ?: queryJoinedRoomMembersEvent().findAll().size + ?: queryJoinedRoomMembersEvent().findAll().size } fun getNumberOfInvitedMembers(): Int { return roomSummary?.invitedMembersCount - ?: queryInvitedRoomMembersEvent().findAll().size + ?: queryInvitedRoomMembersEvent().findAll().size } fun getNumberOfMembers(): Int { @@ -111,7 +110,7 @@ internal class RoomMembers(private val realm: Realm, * @return a roomMember id list of joined or invited members. */ fun getActiveRoomMemberIds(): List { - return getRoomMemberIdsFiltered { it.membership == Membership.JOIN || it.membership == Membership.INVITE } + return queryActiveRoomMembersEvent().findAll().map { it.userId } } /** @@ -120,21 +119,6 @@ internal class RoomMembers(private val realm: Realm, * @return a roomMember id list of joined members. */ fun getJoinedRoomMemberIds(): List { - return getRoomMemberIdsFiltered { it.membership == Membership.JOIN } - } - - /* ========================================================================================== - * Private - * ========================================================================================== */ - - private fun getRoomMemberIdsFiltered(predicate: (RoomMember) -> Boolean): List { - return RoomMembers(realm, roomId) - .queryRoomMembersEvent() - .findAll() - .map { it.asDomain() } - .associateBy { it.stateKey!! } - .filterValues { predicate(it.content.toModel()!!) } - .keys - .toList() + return queryJoinedRoomMembersEvent().findAll().map { it.userId } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 7030509bfc..3727d1f5b0 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 @@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.kotlin.createObject @@ -154,9 +155,6 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy for (event in receivedChunk.events) { event.eventId?.also { eventIds.add(it) } currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked()) - UserEntityFactory.createOrNull(event)?.also { - realm.insertOrUpdate(it) - } } // Then we merge chunks if needed if (currentChunk != prevChunk && prevChunk != null) { @@ -175,9 +173,6 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy roomEntity.addOrUpdate(currentChunk) for (stateEvent in receivedChunk.stateEvents) { roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked()) - UserEntityFactory.createOrNull(stateEvent)?.also { - realm.insertOrUpdate(it) - } } currentChunk.updateSenderDataFor(eventIds) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index a080a5158e..ea59cc1fd2 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 @@ -33,6 +33,7 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater +import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.sync.model.* @@ -46,7 +47,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val roomSummaryUpdater: RoomSummaryUpdater, private val roomTagHandler: RoomTagHandler, private val roomFullyReadHandler: RoomFullyReadHandler, - private val cryptoService: DefaultCryptoService) { + private val cryptoService: DefaultCryptoService, + private val roomMemberEventHandler: RoomMemberEventHandler) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -119,9 +121,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex) // Give info to crypto module cryptoService.onStateEvent(roomId, event) - UserEntityFactory.createOrNull(event)?.also { - realm.insertOrUpdate(it) - } + roomMemberEventHandler.handle(realm, roomId, event) } } if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) { @@ -206,9 +206,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle Timber.v("Can't find corresponding local echo for tx:$it") } } - UserEntityFactory.createOrNull(event)?.also { - realm.insertOrUpdate(it) - } + roomMemberEventHandler.handle(realm, roomEntity.roomId, event) } chunkEntity.updateSenderDataFor(eventIds) return chunkEntity diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index 35988e6c6f..571c9b4c27 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.api.pushrules.RuleSetKey import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.* @@ -69,9 +70,9 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc var hasUpdate = false monarchy.doWithRealm { realm -> invites.forEach { (roomId, _) -> - val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId) + val myUserStateEvent = RoomMembers(realm, roomId).getLastStateEvent(userId) val inviterId = myUserStateEvent?.sender - val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() } + val myUserRoomMember: RoomMemberContent? = myUserStateEvent?.let { it.asDomain().content?.toModel() } val isDirect = myUserRoomMember?.isDirect if (inviterId != null && inviterId != userId && isDirect == true) { directChats diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt index 2ded32b7db..f931db1cff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt @@ -16,27 +16,16 @@ package im.vector.matrix.android.internal.session.user -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.internal.database.model.UserEntity internal object UserEntityFactory { - fun createOrNull(event: Event): UserEntity? { - if (event.type != EventType.STATE_ROOM_MEMBER) { - return null - } - val roomMember = event.content.toModel() ?: return null - // We only use JOIN and INVITED memberships to create User data - if (roomMember.membership != Membership.JOIN && roomMember.membership != Membership.INVITE) { - return null - } - return UserEntity(event.stateKey ?: "", - roomMember.displayName ?: "", - roomMember.avatarUrl ?: "" + fun create(userId: String, roomMember: RoomMemberContent): UserEntity { + return UserEntity( + userId = userId, + displayName = roomMember.displayName ?: "", + avatarUrl = roomMember.avatarUrl ?: "" ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt similarity index 80% rename from vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt rename to vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt index 53a87fe27a..1c8dc99196 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt @@ -14,23 +14,23 @@ * limitations under the License. */ -package im.vector.riotx.features.autocomplete.user +package im.vector.riotx.features.autocomplete.member import com.airbnb.epoxy.TypedEpoxyController -import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.autocomplete.autocompleteMatrixItem import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject -class AutocompleteUserController @Inject constructor() : TypedEpoxyController>() { +class AutocompleteMemberController @Inject constructor() : TypedEpoxyController>() { - var listener: AutocompleteClickListener? = null + var listener: AutocompleteClickListener? = null @Inject lateinit var avatarRenderer: AvatarRenderer - override fun buildModels(data: List?) { + override fun buildModels(data: List?) { if (data.isNullOrEmpty()) { return } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt similarity index 66% rename from vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt rename to vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt index 01dceb5399..b39d1d9b44 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -14,20 +14,20 @@ * limitations under the License. */ -package im.vector.riotx.features.autocomplete.user +package im.vector.riotx.features.autocomplete.member import android.content.Context import androidx.recyclerview.widget.RecyclerView import com.airbnb.mvrx.Async import com.airbnb.mvrx.Success import com.otaliastudios.autocomplete.RecyclerViewPresenter -import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.riotx.features.autocomplete.AutocompleteClickListener import javax.inject.Inject -class AutocompleteUserPresenter @Inject constructor(context: Context, - private val controller: AutocompleteUserController -) : RecyclerViewPresenter(context), AutocompleteClickListener { +class AutocompleteMemberPresenter @Inject constructor(context: Context, + private val controller: AutocompleteMemberController +) : RecyclerViewPresenter(context), AutocompleteClickListener { var callback: Callback? = null @@ -41,21 +41,21 @@ class AutocompleteUserPresenter @Inject constructor(context: Context, return controller.adapter } - override fun onItemClick(t: User) { + override fun onItemClick(t: RoomMember) { dispatchClick(t) } override fun onQuery(query: CharSequence?) { - callback?.onQueryUsers(query) + callback?.onQueryMembers(query) } - fun render(users: Async>) { - if (users is Success) { - controller.setData(users()) + fun render(members: Async>) { + if (members is Success) { + controller.setData(members()) } } interface Callback { - fun onQueryUsers(query: CharSequence?) + fun onQueryMembers(query: CharSequence?) } } 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 334445870c..eff9df9d18 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 @@ -61,6 +61,7 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState @@ -88,7 +89,7 @@ import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresente import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter -import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter +import im.vector.riotx.features.autocomplete.member.AutocompleteMemberPresenter import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.getColorFromUserId @@ -144,7 +145,7 @@ class RoomDetailFragment @Inject constructor( private val timelineEventController: TimelineEventController, private val commandAutocompletePolicy: CommandAutocompletePolicy, private val autocompleteCommandPresenter: AutocompleteCommandPresenter, - private val autocompleteUserPresenter: AutocompleteUserPresenter, + private val autocompleteMemberPresenter: AutocompleteMemberPresenter, private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val autocompleteGroupPresenter: AutocompleteGroupPresenter, private val permalinkHandler: PermalinkHandler, @@ -156,7 +157,7 @@ class RoomDetailFragment @Inject constructor( ) : VectorBaseFragment(), TimelineEventController.Callback, - AutocompleteUserPresenter.Callback, + AutocompleteMemberPresenter.Callback, AutocompleteRoomPresenter.Callback, AutocompleteGroupPresenter.Callback, VectorInviteView.Callback, @@ -693,14 +694,14 @@ class RoomDetailFragment @Inject constructor( }) .build() - autocompleteUserPresenter.callback = this - Autocomplete.on(composerLayout.composerEditText) + autocompleteMemberPresenter.callback = this + Autocomplete.on(composerLayout.composerEditText) .with(CharPolicy('@', true)) - .with(autocompleteUserPresenter) + .with(autocompleteMemberPresenter) .with(elevation) .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: User): Boolean { + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: RoomMember): Boolean { // Detect last '@' and remove it var startIndex = editable.lastIndexOf("@") if (startIndex == -1) { @@ -834,7 +835,7 @@ class RoomDetailFragment @Inject constructor( } private fun renderTextComposerState(state: TextComposerViewState) { - autocompleteUserPresenter.render(state.asyncUsers) + autocompleteMemberPresenter.render(state.asyncMembers) autocompleteRoomPresenter.render(state.asyncRooms) autocompleteGroupPresenter.render(state.asyncGroups) } @@ -1163,9 +1164,9 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } - // AutocompleteUserPresenter.Callback + // AutocompleteMemberPresenter.Callback - override fun onQueryUsers(query: CharSequence?) { + override fun onQueryMembers(query: CharSequence?) { textComposerViewModel.handle(TextComposerAction.QueryUsers(query)) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt index f7ec78c6c4..25f92d4ae2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt @@ -25,6 +25,8 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.rx.rx @@ -90,17 +92,15 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: } private fun observeUsersQuery() { - Observable.combineLatest, Option, List>( - room.rx().liveRoomMemberIds(), + Observable.combineLatest, Option, List>( + room.rx().liveRoomMembers(Membership.activeMemberships()), usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), - BiFunction { roomMemberIds, query -> - val users = roomMemberIds.mapNotNull { session.getUser(it) } - + BiFunction { roomMembers, query -> val filter = query.orNull() if (filter.isNullOrBlank()) { - users + roomMembers } else { - users.filter { + roomMembers.filter { it.displayName?.contains(filter, ignoreCase = true) ?: false } } @@ -108,7 +108,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: } ).execute { async -> copy( - asyncUsers = async + asyncMembers = async ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt index e863970afe..0320818f67 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt @@ -20,12 +20,13 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.features.home.room.detail.RoomDetailArgs data class TextComposerViewState(val roomId: String, - val asyncUsers: Async> = Uninitialized, + val asyncMembers: Async> = Uninitialized, val asyncRooms: Async> = Uninitialized, val asyncGroups: Async> = Uninitialized ) : MvRxState { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 9cb045c01e..a201890912 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -128,8 +128,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active } private fun formatRoomMemberEvent(event: Event, senderName: String?): String? { - val eventContent: RoomMember? = event.getClearContent().toModel() - val prevEventContent: RoomMember? = event.prevContent.toModel() + val eventContent: RoomMemberContent? = event.getClearContent().toModel() + val prevEventContent: RoomMemberContent? = event.prevContent.toModel() val isMembershipEvent = prevEventContent?.membership != eventContent?.membership return if (isMembershipEvent) { buildMembershipNotice(event, senderName, eventContent, prevEventContent) @@ -166,7 +166,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active ?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName) } - private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String { + private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String { val displayText = StringBuilder() // Check display name has been changed if (eventContent?.displayName != prevEventContent?.displayName) { @@ -198,7 +198,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active return displayText.toString() } - private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { + private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String? { val senderDisplayName = senderName ?: event.senderId ?: "" val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: "" return when (eventContent?.membership) { diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index e38e7d548a..ef620a7df3 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getEditedEventId import im.vector.matrix.android.api.session.room.timeline.getLastMessageBody @@ -163,7 +164,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? { - val content = event.content?.toModel() ?: return null + val content = event.content?.toModel() ?: return null val roomId = event.roomId ?: return null val dName = event.senderId?.let { session.getUser(it)?.displayName } if (Membership.INVITE == content.membership) { From 833a5a37a25147d52e1e39522238ff8bdb9b8a74 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 27 Dec 2019 10:24:58 +0100 Subject: [PATCH 041/105] Pill: fix blink and clean files --- .../android/api/session/room/model/Membership.kt | 3 --- .../database/helper/TimelineEventEntityHelper.kt | 1 - .../database/query/RoomMemberEntityQueries.kt | 2 -- .../internal/session/room/RoomAvatarResolver.kt | 3 --- .../room/membership/LoadRoomMembersTask.kt | 1 - .../room/membership/RoomMemberEntityFactory.kt | 2 +- .../room/membership/RoomMemberEventHandler.kt | 3 --- .../session/room/membership/RoomMembers.kt | 3 --- .../room/timeline/TokenChunkEventPersistor.kt | 2 -- .../internal/session/sync/RoomSyncHandler.kt | 1 - .../session/sync/UserAccountDataSyncHandler.kt | 1 - .../vector/riotx/core/epoxy/VectorEpoxyModel.kt | 11 +++++++++++ .../bottomsheet/BottomSheetMessagePreviewItem.kt | 2 +- .../vector/riotx/features/home/AvatarRenderer.kt | 8 ++++++++ .../home/room/detail/RoomDetailFragment.kt | 1 - .../detail/composer/TextComposerViewModel.kt | 1 - .../detail/composer/TextComposerViewState.kt | 1 - .../room/detail/timeline/item/BaseEventItem.kt | 1 + .../room/detail/timeline/item/MessageTextItem.kt | 2 +- .../detail/timeline/tools/EventRenderingTools.kt | 9 +++------ .../vector/riotx/features/html/PillImageSpan.kt | 16 +++++++++------- .../notifications/NotifiableEventResolver.kt | 1 - 22 files changed, 35 insertions(+), 40 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt index 4d35d3b4dd..93eb54fbd3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt @@ -49,7 +49,4 @@ enum class Membership(val value: String) { return listOf(INVITE, JOIN) } } - } - - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt index c5e2aef910..51775f9e9d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.model.* diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt index 3eb1da87a1..2ddade0048 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt @@ -18,8 +18,6 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.internal.database.model.RoomMemberEntity import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields -import im.vector.matrix.android.internal.database.model.UserEntity -import im.vector.matrix.android.internal.database.model.UserEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where 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 acfb57bd46..0bb2dc0f27 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt @@ -20,10 +20,8 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomAvatarContent -import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where @@ -59,5 +57,4 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona } return res } - } 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 6affd120a8..64cbcb3527 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 @@ -26,7 +26,6 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.sync.SyncTokenStore -import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt index 933431b77f..51df244401 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt @@ -22,7 +22,7 @@ import im.vector.matrix.android.internal.database.model.RoomMemberEntity internal object RoomMemberEntityFactory { fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberEntity { - val primaryKey = "${roomId}_${userId}" + val primaryKey = "${roomId}_$userId" return RoomMemberEntity( primaryKey = primaryKey, userId = userId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt index 73c4c8cdc1..16d6aacbc9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt @@ -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.toModel import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.internal.session.user.UserEntityFactory import io.realm.Realm @@ -42,6 +41,4 @@ internal class RoomMemberEventHandler @Inject constructor() { } return true } - - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index ca397a0608..f2dd733978 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -17,10 +17,7 @@ package im.vector.matrix.android.internal.session.room.membership import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomMemberEntity 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 3727d1f5b0..df52e9a33a 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 @@ -28,8 +28,6 @@ import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler -import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.kotlin.createObject import timber.log.Timber 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 ea59cc1fd2..f4efa291b8 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 @@ -37,7 +37,6 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMemberEvent import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.sync.model.* -import im.vector.matrix.android.internal.session.user.UserEntityFactory import io.realm.Realm import io.realm.kotlin.createObject import timber.log.Timber diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index 571c9b4c27..9bc8c86be5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -20,7 +20,6 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.api.pushrules.RuleSetKey import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.mapper.asDomain diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/VectorEpoxyModel.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/VectorEpoxyModel.kt index 667ccb1bd0..654e4c605a 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/VectorEpoxyModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/VectorEpoxyModel.kt @@ -18,14 +18,25 @@ package im.vector.riotx.core.epoxy import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.VisibilityState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren /** * EpoxyModelWithHolder which can listen to visibility state change */ abstract class VectorEpoxyModel : EpoxyModelWithHolder() { + protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null + override fun unbind(holder: H) { + coroutineScope.coroutineContext.cancelChildren() + super.unbind(holder) + } + override fun onVisibilityStateChanged(visibilityState: Int, view: H) { onModelVisibilityStateChangedListener?.onVisibilityStateChanged(visibilityState) super.onVisibilityStateChanged(visibilityState, view) diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index 7b79ce8549..e5ffd5f350 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -51,7 +51,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel() { holder.messageView.setOnClickListener(attributes.itemClickListener) holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) if (searchForPills) { - message?.findPillsAndProcess { it.bind(holder.messageView) } + message?.findPillsAndProcess(coroutineScope) { it.bind(holder.messageView) } } val textFuture = PrecomputedTextCompat.getTextFuture( message ?: "", diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt index 492248985e..043763fd8e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt @@ -25,14 +25,11 @@ import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.utils.isValidUrl import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.html.PillImageSpan -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import me.saket.bettermovementmethod.BetterLinkMovementMethod -fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) { - GlobalScope.launch(Dispatchers.Main) { +fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillImageSpan) -> Unit) { + scope.launch(Dispatchers.Main) { withContext(Dispatchers.IO) { toSpannable().let { spannable -> spannable.getSpans(0, spannable.length, PillImageSpan::class.java) diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt index a609541a62..3d3dcbea94 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt @@ -88,25 +88,27 @@ class PillImageSpan(private val glideRequests: GlideRequests, } internal fun updateAvatarDrawable(drawable: Drawable?) { - pillDrawable.apply { - chipIcon = drawable - } - tv?.get()?.apply { - invalidate() - } + pillDrawable.chipIcon = drawable + tv?.get()?.invalidate() } // Private methods ***************************************************************************** private fun createChipDrawable(): ChipDrawable { val textPadding = context.resources.getDimension(R.dimen.pill_text_padding) + val icon = try { + avatarRenderer.getCachedDrawable(glideRequests, matrixItem) + } catch (exception: Exception) { + avatarRenderer.getPlaceholderDrawable(context, matrixItem) + } + return ChipDrawable.createFromResource(context, R.xml.pill_view).apply { text = matrixItem.getBestName() textEndPadding = textPadding textStartPadding = textPadding setChipMinHeightResource(R.dimen.pill_min_height) setChipIconSizeResource(R.dimen.pill_avatar_size) - chipIcon = avatarRenderer.getPlaceholderDrawable(context, matrixItem) + chipIcon = icon setBounds(0, 0, intrinsicWidth, intrinsicHeight) } } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index ef620a7df3..11d770adc4 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -23,7 +23,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.toModel import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getEditedEventId From 037bf45884210a4675a2f801af1d7bfd6adb0f6d Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 27 Dec 2019 17:09:57 +0100 Subject: [PATCH 042/105] Sync: use foreground service on every android version --- .../riotx/core/services/VectorSyncService.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 31b56c4e47..b6b8fbf06a 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 @@ -45,15 +45,13 @@ class VectorSyncService : SyncService() { } override fun onStart(isInitialSync: Boolean) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationSubtitleRes = if (isInitialSync) { - R.string.notification_initial_sync - } else { - R.string.notification_listening_for_events - } - val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false) - startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) + val notificationSubtitleRes = if (isInitialSync) { + R.string.notification_initial_sync + } else { + R.string.notification_listening_for_events } + val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false) + startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) } override fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long) { From 8109262cbb2f0286ce2c9b562e28bbc7c62274e9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 27 Dec 2019 17:16:30 +0100 Subject: [PATCH 043/105] Home: fix double tab selection --- .../riotx/features/home/HomeDetailFragment.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index b9d3e3c95e..2913ce80fe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -66,6 +66,11 @@ class HomeDetailFragment @Inject constructor( setupToolbar() setupKeysBackupBanner() + withState(viewModel) { + // Update the navigation view if needed (for when we restore the tabs) + bottomNavigationView.selectedItemId = it.displayMode.toMenuId() + } + viewModel.selectSubscribe(this, HomeDetailViewState::groupSummary) { groupSummary -> onGroupChange(groupSummary.orNull()) } @@ -127,7 +132,6 @@ class HomeDetailFragment @Inject constructor( private fun setupBottomNavigationView() { bottomNavigationView.setOnNavigationItemSelectedListener { val displayMode = when (it.itemId) { - R.id.bottom_action_home -> RoomListDisplayMode.HOME R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE R.id.bottom_action_rooms -> RoomListDisplayMode.ROOMS else -> RoomListDisplayMode.HOME @@ -149,12 +153,6 @@ class HomeDetailFragment @Inject constructor( private fun switchDisplayMode(displayMode: RoomListDisplayMode) { groupToolbarTitleView.setText(displayMode.titleRes) updateSelectedFragment(displayMode) - // Update the navigation view (for when we restore the tabs) - bottomNavigationView.selectedItemId = when (displayMode) { - RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people - RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms - else -> R.id.bottom_action_home - } } private fun updateSelectedFragment(displayMode: RoomListDisplayMode) { @@ -194,4 +192,11 @@ class HomeDetailFragment @Inject constructor( unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms)) syncStateView.render(it.syncState) } + + private fun RoomListDisplayMode.toMenuId() = when (this) { + RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people + RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms + else -> R.id.bottom_action_home + } + } From 92f4288d3e6615858672bdc7eebb98b365d0d8b5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 27 Dec 2019 17:16:44 +0100 Subject: [PATCH 044/105] Realm: update realm lib version --- 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 ab5f122dbc..799a619748 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -10,7 +10,7 @@ buildscript { jcenter() } dependencies { - classpath "io.realm:realm-gradle-plugin:5.12.0" + classpath "io.realm:realm-gradle-plugin:6.0.2" } } From 679417332145f4bd29e2119a2245eab758cdecbb Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 27 Dec 2019 18:54:07 +0100 Subject: [PATCH 045/105] Room detail: fix crash with banner --- .../riotx/features/home/room/detail/RoomDetailFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c0ae1ec252..fa483d675c 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 @@ -563,7 +563,7 @@ class RoomDetailFragment @Inject constructor( } } } - jumpToReadMarkerView.isVisible = showJumpToUnreadBanner + jumpToReadMarkerView?.isVisible = showJumpToUnreadBanner } } } From cba7e460ebb9a8a030286bfe25a77fff05e5ad2d Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 27 Dec 2019 18:54:28 +0100 Subject: [PATCH 046/105] Action bottom sheet: fix deprecated constraints --- vector/src/main/res/layout/item_bottom_sheet_action.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/layout/item_bottom_sheet_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml index 0ad7a211da..db01db0a2f 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_action.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml @@ -38,7 +38,7 @@ From 6ad914154a107e6d2e4297cec7a69801f29b2179 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 30 Dec 2019 10:46:25 +0100 Subject: [PATCH 047/105] Update some libs --- vector/build.gradle | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index c8d474088f..410e478ee2 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -216,8 +216,8 @@ android { dependencies { - def epoxy_version = '3.8.0' - def fragment_version = '1.2.0-rc01' + def epoxy_version = '3.9.0' + def fragment_version = '1.2.0-rc04' def arrow_version = "0.8.2" def coroutines_version = "1.3.2" def markwon_version = '4.1.2' @@ -225,7 +225,7 @@ dependencies { def glide_version = '4.10.0' def moshi_version = '1.8.0' def daggerVersion = '2.24' - def autofill_version = "1.0.0-rc01" + def autofill_version = "1.0.0" implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") @@ -239,8 +239,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.1.0' implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version" - //Do not use beta2 at the moment, as it breaks things - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation 'androidx.core:core-ktx:1.1.0' implementation "org.threeten:threetenbp:1.4.0:no-tzdb" @@ -276,10 +275,10 @@ dependencies { implementation 'com.airbnb.android:mvrx:1.3.0' // Work - implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01" + implementation "androidx.work:work-runtime-ktx:2.3.0-beta02" // Paging - implementation "androidx.paging:paging-runtime-ktx:2.1.0" + implementation "androidx.paging:paging-runtime-ktx:2.1.1" // Functional Programming implementation "io.arrow-kt:arrow-core:$arrow_version" @@ -289,7 +288,7 @@ dependencies { // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.1.0-beta01' + implementation 'com.google.android.material:material:1.2.0-alpha03' implementation 'me.gujun.android:span:1.7' implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:html:$markwon_version" From 03fd474aa8e4bd3ae911030ee7e072cd984ceac7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 30 Dec 2019 19:53:36 +0100 Subject: [PATCH 048/105] Member events: try to cache (WIP) --- .../session/room/timeline/ChunkEntityTest.kt | 17 ++- .../session/room/timeline/RoomDataHelper.kt | 22 --- .../database/helper/ChunkEntityHelper.kt | 70 ++++----- .../database/helper/RoomMembersCache.kt | 135 ++++++++++++++++++ .../helper/TimelineEventEntityHelper.kt | 26 ++-- .../database/model/TimelineEventEntity.kt | 2 +- .../query/TimelineEventEntityQueries.kt | 2 +- .../room/timeline/TokenChunkEventPersistor.kt | 8 +- .../internal/session/sync/RoomSyncHandler.kt | 10 +- 9 files changed, 204 insertions(+), 88 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomMembersCache.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 592086b0ec..5cca9f6696 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -19,7 +19,11 @@ package im.vector.matrix.android.session.room.timeline import androidx.test.ext.junit.runners.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.isUnlinked +import im.vector.matrix.android.internal.database.helper.lastStateIndex +import im.vector.matrix.android.internal.database.helper.merge import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection @@ -197,4 +201,15 @@ internal class ChunkEntityTest : InstrumentedTest { chunk1.nextToken shouldEqual nextToken } } + + private fun ChunkEntity.addAll(roomId: String, + events: List, + direction: PaginationDirection, + stateIndexOffset: Int = 0, + // Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk) + isUnlinked: Boolean = false) { + events.forEach { event -> + add(roomId, event, direction, stateIndexOffset, isUnlinked) + } + } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt index 8a8ee11854..dd4daee9cd 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.session.room.timeline -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -25,12 +24,6 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.internal.database.helper.addAll -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection -import io.realm.kotlin.createObject import kotlin.random.Random object RoomDataHelper { @@ -73,19 +66,4 @@ object RoomDataHelper { val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) } - - fun fakeInitialSync(monarchy: Monarchy, roomId: String) { - monarchy.runTransactionSync { realm -> - val roomEntity = realm.createObject(roomId) - roomEntity.membership = Membership.JOIN - val eventList = createFakeListOfEvents(10) - val chunkEntity = realm.createObject().apply { - nextToken = null - prevToken = Random.nextLong(System.currentTimeMillis()).toString() - isLastForward = true - } - chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS) - roomEntity.addOrUpdate(chunkEntity) - } - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index f05fa01444..f60c37512c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import io.realm.Sort +import io.realm.kotlin.createObject // By default if a chunk is empty we consider it unlinked internal fun ChunkEntity.isUnlinked(): Boolean { @@ -65,38 +66,22 @@ internal fun ChunkEntity.merge(roomId: String, this.isLastBackward = chunkToMerge.isLastBackward eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) } - val events = eventsToMerge.mapNotNull { it.root?.asDomain() } - val eventIds = ArrayList() - events.forEach { event -> - add(roomId, event, direction, isUnlinked = isUnlinked) - if (event.eventId != null) { - eventIds.add(event.eventId) - } - } - updateSenderDataFor(eventIds) + val timelineEvents = eventsToMerge + .mapNotNull { + val event = it.root?.asDomain() ?: return@mapNotNull null + add(roomId, event, direction, isUnlinked = isUnlinked) + } + updateSenderDataFor(timelineEvents) } -internal fun ChunkEntity.addAll(roomId: String, - events: List, - direction: PaginationDirection, - stateIndexOffset: Int = 0, - // Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk) - isUnlinked: Boolean = false) { - assertIsManaged() - val eventIds = ArrayList() - events.forEach { event -> - add(roomId, event, direction, stateIndexOffset, isUnlinked) - if (event.eventId != null) { - eventIds.add(event.eventId) - } - } - updateSenderDataFor(eventIds) -} - -internal fun ChunkEntity.updateSenderDataFor(eventIds: List) { - for (eventId in eventIds) { - val timelineEventEntity = timelineEvents.find(eventId) ?: continue - timelineEventEntity.updateSenderData() +internal fun ChunkEntity.updateSenderDataFor(events: List) { + val cache = RoomMembersCache() + events.forEach { + val result = cache.get(it) + it.isUniqueDisplayName = result.isUniqueDisplayName + it.senderAvatar = result.senderAvatar + it.senderName = result.senderName + it.senderMembershipEventId = result.senderMembershipEventId } } @@ -104,10 +89,10 @@ internal fun ChunkEntity.add(roomId: String, event: Event, direction: PaginationDirection, stateIndexOffset: Int = 0, - isUnlinked: Boolean = false) { + isUnlinked: Boolean = false): TimelineEventEntity? { assertIsManaged() if (event.eventId != null && timelineEvents.find(event.eventId) != null) { - return + return null } var currentDisplayIndex = lastDisplayIndex(direction, 0) if (direction == PaginationDirection.FORWARDS) { @@ -134,7 +119,9 @@ internal fun ChunkEntity.add(roomId: String, val senderId = event.senderId ?: "" val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: ReadReceiptsSummaryEntity(eventId, roomId) + ?: realm.createObject(eventId).apply { + this.roomId = roomId + } // Update RR for the sender of a new message with a dummy one @@ -151,13 +138,15 @@ internal fun ChunkEntity.add(roomId: String, } } - val eventEntity = TimelineEventEntity(localId).also { - it.root = event.toEntity(roomId).apply { - this.stateIndex = currentStateIndex - this.isUnlinked = isUnlinked - this.displayIndex = currentDisplayIndex - this.sendState = SendState.SYNCED - } + val rootEvent = event.toEntity(roomId).apply { + this.stateIndex = currentStateIndex + this.isUnlinked = isUnlinked + this.displayIndex = currentDisplayIndex + this.sendState = SendState.SYNCED + } + val eventEntity = realm.createObject().also { + it.localId = localId + it.root = realm.copyToRealm(rootEvent) it.eventId = eventId it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() @@ -165,6 +154,7 @@ internal fun ChunkEntity.add(roomId: String, } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) + return eventEntity } internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomMembersCache.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomMembersCache.kt new file mode 100644 index 0000000000..fb2c367e64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomMembersCache.kt @@ -0,0 +1,135 @@ +/* + * 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.room.membership.RoomMembers +import io.realm.RealmList +import io.realm.RealmQuery +import timber.log.Timber + +/** + * This is an internal cache to avoid querying all the time the room member events + */ +internal class RoomMembersCache { + + 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 get(timelineEventEntity: TimelineEventEntity): Value { + val key = Key( + roomId = timelineEventEntity.roomId, + stateIndex = timelineEventEntity.root?.stateIndex ?: 0, + senderId = timelineEventEntity.root?.sender ?: "" + ) + val result: Value + val start = System.currentTimeMillis() + result = values.getOrPut(key) { + doQueryAndBuildValue(timelineEventEntity) + } + val end = System.currentTimeMillis() + Timber.v("Get value took: ${end - start} millis") + return result + } + + private fun doQueryAndBuildValue(timelineEventEntity: TimelineEventEntity): Value { + return timelineEventEntity.computeValue() + } + + private fun RealmList.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery { + return where() + .equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender) + .equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER) + .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked) + } + + private fun TimelineEventEntity.computeValue(): Value { + assertIsManaged() + val result = Value() + + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return result + val stateIndex = root?.stateIndex ?: return result + val senderId = root?.sender ?: return result + val chunkEntity = chunk?.firstOrNull() ?: return result + val isUnlinked = chunkEntity.isUnlinked() + var senderMembershipEvent: EventEntity? + var senderRoomMemberContent: String? + var senderRoomMemberPrevContent: String? + + if (stateIndex <= 0) { + senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root + senderRoomMemberContent = senderMembershipEvent?.prevContent + senderRoomMemberPrevContent = senderMembershipEvent?.content + } else { + senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root + senderRoomMemberContent = senderMembershipEvent?.content + senderRoomMemberPrevContent = senderMembershipEvent?.prevContent + } + + // We fallback to untimelinedStateEvents if we can't find membership events in timeline + if (senderMembershipEvent == null) { + senderMembershipEvent = roomEntity.untimelinedStateEvents + .where() + .equalTo(EventEntityFields.STATE_KEY, senderId) + .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) + .prev(since = stateIndex) + senderRoomMemberContent = senderMembershipEvent?.content + senderRoomMemberPrevContent = senderMembershipEvent?.prevContent + } + + ContentMapper.map(senderRoomMemberContent).toModel()?.also { + result.senderAvatar = it.avatarUrl + result.senderName = it.displayName + result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + } + // We try to fallback on prev content if we got a room member state events with null fields + if (root?.type == EventType.STATE_ROOM_MEMBER) { + ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also { + if (result.senderAvatar == null && it.avatarUrl != null) { + result.senderAvatar = it.avatarUrl + } + if (result.senderName == null && it.displayName != null) { + result.senderName = it.displayName + result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + } + } + } + result.senderMembershipEventId = senderMembershipEvent?.eventId + return result + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt index 51775f9e9d..c7bedaee21 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt @@ -40,20 +40,18 @@ internal fun TimelineEventEntity.updateSenderData() { var senderMembershipEvent: EventEntity? var senderRoomMemberContent: String? var senderRoomMemberPrevContent: String? - when { - stateIndex <= 0 -> { - senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root - senderRoomMemberContent = senderMembershipEvent?.prevContent - senderRoomMemberPrevContent = senderMembershipEvent?.content - } - else -> { - senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root - senderRoomMemberContent = senderMembershipEvent?.content - senderRoomMemberPrevContent = senderMembershipEvent?.prevContent - } + + 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 +// We fallback to untimelinedStateEvents if we can't find membership events in timeline if (senderMembershipEvent == null) { senderMembershipEvent = roomEntity.untimelinedStateEvents .where() @@ -70,7 +68,7 @@ internal fun TimelineEventEntity.updateSenderData() { this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) } - // We try to fallback on prev content if we got a room member state events with null fields +// We try to fallback on prev content if we got a room member state events with null fields if (root?.type == EventType.STATE_ROOM_MEMBER) { ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also { if (this.senderAvatar == null && it.avatarUrl != null) { @@ -82,7 +80,7 @@ internal fun TimelineEventEntity.updateSenderData() { } } } - this.senderMembershipEvent = senderMembershipEvent + this.senderMembershipEventId = senderMembershipEvent?.eventId } internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt index 235910b1ea..22f4b9c506 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt @@ -29,7 +29,7 @@ internal open class TimelineEventEntity(var localId: Long = 0, var senderName: String? = null, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, - var senderMembershipEvent: EventEntity? = null, + var senderMembershipEventId: String? = null, var readReceipts: ReadReceiptsSummaryEntity? = null ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 3bd035c0b1..221e8ccb46 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -54,7 +54,7 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm, internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List { return realm.where() - .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT.EVENT_ID, senderMembershipEventId) + .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId) .findAll() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index df52e9a33a..342a40052c 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 @@ -149,10 +149,8 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy currentChunk.isLastBackward = true } else if (!shouldSkip) { Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") - val eventIds = ArrayList(receivedChunk.events.size) - for (event in receivedChunk.events) { - event.eventId?.also { eventIds.add(it) } - currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked()) + val timelineEvents = receivedChunk.events.mapNotNull { + currentChunk.add(roomId, it, direction, isUnlinked = currentChunk.isUnlinked()) } // Then we merge chunks if needed if (currentChunk != prevChunk && prevChunk != null) { @@ -172,7 +170,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy for (stateEvent in receivedChunk.stateEvents) { roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked()) } - currentChunk.updateSenderDataFor(eventIds) + currentChunk.updateSenderDataFor(timelineEvents) } } return if (receivedChunk.events.isEmpty()) { 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 f4efa291b8..f3c0cd9511 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where @@ -189,10 +190,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle lastChunk?.isLastForward = false chunkEntity.isLastForward = true - val eventIds = ArrayList(eventList.size) + val timelineEvents = ArrayList(eventList.size) for (event in eventList) { - event.eventId?.also { eventIds.add(it) } - chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset) + chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also { + timelineEvents.add(it) + } // Give info to crypto module cryptoService.onLiveEvent(roomEntity.roomId, event) // Try to remove local echo @@ -207,7 +209,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } roomMemberEventHandler.handle(realm, roomEntity.roomId, event) } - chunkEntity.updateSenderDataFor(eventIds) + chunkEntity.updateSenderDataFor(timelineEvents) return chunkEntity } From 8156b754c173774ecbfdd28dbdabe6ec4d80f840 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 30 Dec 2019 19:54:39 +0100 Subject: [PATCH 049/105] RecyclerView: introduce view pool --- .../main/java/im/vector/riotx/core/di/ScreenComponent.kt | 3 ++- .../src/main/java/im/vector/riotx/core/di/ScreenModule.kt | 6 ++++++ .../java/im/vector/riotx/core/extensions/RecyclerView.kt | 6 +++++- .../riotx/features/home/room/list/RoomListFragment.kt | 6 ++++-- .../room/list/actions/RoomListQuickActionsBottomSheet.kt | 3 ++- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index e0b14af9d0..aec3372098 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -63,7 +63,8 @@ import im.vector.riotx.features.ui.UiStateRepository ViewModelModule::class, FragmentModule::class, HomeModule::class, - RoomListModule::class + RoomListModule::class, + ScreenModule::class ] ) @ScreenScope diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenModule.kt index 1073a59f7c..56fac34f1e 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenModule.kt @@ -17,6 +17,7 @@ package im.vector.riotx.core.di import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView import dagger.Module import dagger.Provides import im.vector.riotx.core.glide.GlideApp @@ -27,4 +28,9 @@ object ScreenModule { @Provides @JvmStatic fun providesGlideRequests(context: AppCompatActivity) = GlideApp.with(context) + + @Provides + @JvmStatic + @ScreenScope + fun providesSharedViewPool() = RecyclerView.RecycledViewPool() } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt index 003045af51..3d247e149c 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt @@ -26,9 +26,13 @@ import com.airbnb.epoxy.EpoxyController */ fun RecyclerView.configureWith(epoxyController: EpoxyController, itemAnimator: RecyclerView.ItemAnimator? = null, + viewPool: RecyclerView.RecycledViewPool? = null, showDivider: Boolean = false, hasFixedSize: Boolean = true) { - layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply { + recycleChildrenOnDetach = viewPool != null + } + setRecycledViewPool(viewPool) itemAnimator?.let { this.itemAnimator = it } if (showDivider) { addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index e272c1423f..122b95aa52 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -59,7 +59,8 @@ data class RoomListParams( class RoomListFragment @Inject constructor( private val roomController: RoomSummaryController, val roomListViewModelFactory: RoomListViewModel.Factory, - private val notificationDrawerManager: NotificationDrawerManager + private val notificationDrawerManager: NotificationDrawerManager, + private val sharedViewPool: RecyclerView.RecycledViewPool ) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { @@ -95,7 +96,6 @@ class RoomListFragment @Inject constructor( setupCreateRoomButton() setupRecyclerView() sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) - roomListViewModel.subscribe { renderState(it) } roomListViewModel.viewEvents .observe() @@ -193,6 +193,8 @@ class RoomListFragment @Inject constructor( val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() roomListView.layoutManager = layoutManager roomListView.itemAnimator = RoomListAnimator() + roomListView.setRecycledViewPool(sharedViewPool) + layoutManager.recycleChildrenOnDetach = true roomController.listener = this modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } roomController.addModelBuildListener(modelBuildListener) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt index 60a26c8151..5fc33ffbe9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt @@ -46,6 +46,7 @@ data class RoomListActionsArgs( class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomListQuickActionsEpoxyController.Listener { private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel + @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool @Inject lateinit var roomListActionsViewModelFactory: RoomListQuickActionsViewModel.Factory @Inject lateinit var roomListActionsEpoxyController: RoomListQuickActionsEpoxyController @Inject lateinit var navigator: Navigator @@ -70,7 +71,7 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) - recyclerView.configureWith(roomListActionsEpoxyController, hasFixedSize = false) + recyclerView.configureWith(roomListActionsEpoxyController, viewPool = sharedViewPool, hasFixedSize = false) // Disable item animation recyclerView.itemAnimator = null roomListActionsEpoxyController.listener = this From 787908287c46c6576075e35c5ff30187ae4f32b3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 31 Dec 2019 08:07:32 +0100 Subject: [PATCH 050/105] Member events: cache all over the session --- .../database/helper/ChunkEntityHelper.kt | 16 +--- .../helper/TimelineEventEntityHelper.kt | 74 +------------------ ...Cache.kt => TimelineEventSenderVisitor.kt} | 41 ++++++---- .../room/membership/LoadRoomMembersTask.kt | 8 +- .../session/room/prune/PruneEventTask.kt | 18 +++-- .../room/timeline/TokenChunkEventPersistor.kt | 11 ++- .../internal/session/sync/RoomSyncHandler.kt | 5 +- 7 files changed, 55 insertions(+), 118 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/{RoomMembersCache.kt => TimelineEventSenderVisitor.kt} (84%) 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 f60c37512c..90236a4f98 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 @@ -47,7 +47,7 @@ internal fun ChunkEntity.deleteOnCascade() { internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, - direction: PaginationDirection) { + direction: PaginationDirection): List { assertIsManaged() val isChunkToMergeUnlinked = chunkToMerge.isUnlinked() val isCurrentChunkUnlinked = this.isUnlinked() @@ -66,23 +66,11 @@ internal fun ChunkEntity.merge(roomId: String, this.isLastBackward = chunkToMerge.isLastBackward eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) } - val timelineEvents = eventsToMerge + return eventsToMerge .mapNotNull { val event = it.root?.asDomain() ?: return@mapNotNull null add(roomId, event, direction, isUnlinked = isUnlinked) } - updateSenderDataFor(timelineEvents) -} - -internal fun ChunkEntity.updateSenderDataFor(events: List) { - val cache = RoomMembersCache() - events.forEach { - val result = cache.get(it) - it.isUniqueDisplayName = result.isUniqueDisplayName - it.senderAvatar = result.senderAvatar - it.senderName = result.senderName - it.senderMembershipEventId = result.senderMembershipEventId - } } internal fun ChunkEntity.add(roomId: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt index c7bedaee21..0bf02aa92f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt @@ -16,72 +16,9 @@ package im.vector.matrix.android.internal.database.helper -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.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.room.membership.RoomMembers +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import io.realm.Realm -import io.realm.RealmList -import io.realm.RealmQuery - -internal fun TimelineEventEntity.updateSenderData() { - assertIsManaged() - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return - val stateIndex = root?.stateIndex ?: return - val senderId = root?.sender ?: return - val chunkEntity = chunk?.firstOrNull() ?: return - val isUnlinked = chunkEntity.isUnlinked() - var senderMembershipEvent: EventEntity? - var senderRoomMemberContent: String? - var senderRoomMemberPrevContent: String? - - 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 { - this.senderAvatar = it.avatarUrl - this.senderName = it.displayName - this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) - } - -// We try to fallback on prev content if we got a room member state events with null fields - if (root?.type == EventType.STATE_ROOM_MEMBER) { - ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also { - if (this.senderAvatar == null && it.avatarUrl != null) { - this.senderAvatar = it.avatarUrl - } - if (this.senderName == null && it.displayName != null) { - this.senderName = it.displayName - this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) - } - } - } - this.senderMembershipEventId = senderMembershipEvent?.eventId -} internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID) @@ -91,10 +28,3 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { currentIdNum.toLong() + 1 } } - -private fun RealmList.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery { - return where() - .equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender) - .equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER) - .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked) -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomMembersCache.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt similarity index 84% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomMembersCache.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt index fb2c367e64..2e8b02d6aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomMembersCache.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt @@ -25,15 +25,17 @@ import im.vector.matrix.android.internal.database.query.next import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.extensions.assertIsManaged +import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.membership.RoomMembers import io.realm.RealmList import io.realm.RealmQuery -import timber.log.Timber +import javax.inject.Inject /** * This is an internal cache to avoid querying all the time the room member events */ -internal class RoomMembersCache { +@SessionScope +internal class TimelineEventSenderVisitor @Inject constructor() { internal data class Key( val roomId: String, @@ -50,24 +52,34 @@ internal class RoomMembersCache { private val values = HashMap() - fun get(timelineEventEntity: TimelineEventEntity): Value { + 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) { val key = Key( roomId = timelineEventEntity.roomId, stateIndex = timelineEventEntity.root?.stateIndex ?: 0, senderId = timelineEventEntity.root?.sender ?: "" ) - val result: Value - val start = System.currentTimeMillis() - result = values.getOrPut(key) { - doQueryAndBuildValue(timelineEventEntity) + 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 } - val end = System.currentTimeMillis() - Timber.v("Get value took: ${end - start} millis") - return result - } - - private fun doQueryAndBuildValue(timelineEventEntity: TimelineEventEntity): Value { - return timelineEventEntity.computeValue() } private fun RealmList.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery { @@ -80,7 +92,6 @@ internal class RoomMembersCache { 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 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 64cbcb3527..dd91875f98 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt @@ -18,8 +18,8 @@ package im.vector.matrix.android.internal.session.room.membership import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor import im.vector.matrix.android.internal.database.helper.addStateEvent -import im.vector.matrix.android.internal.database.helper.updateSenderData import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest @@ -44,7 +44,8 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP private val monarchy: Monarchy, private val syncTokenStore: SyncTokenStore, private val roomSummaryUpdater: RoomSummaryUpdater, - private val roomMemberEventHandler: RoomMemberEventHandler + private val roomMemberEventHandler: RoomMemberEventHandler, + private val timelineEventSenderVisitor: TimelineEventSenderVisitor ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { @@ -68,8 +69,9 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP roomEntity.addStateEvent(roomMemberEvent) roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) } + timelineEventSenderVisitor.clear() roomEntity.chunks.flatMap { it.timelineEvents }.forEach { - it.updateSenderData() + timelineEventSenderVisitor.visit(it) } roomEntity.areAllMembersLoaded = true roomSummaryUpdater.update(realm, roomId, updateMembers = true) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt index de3eb1eab2..8228136f10 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.events.model.UnsignedData -import im.vector.matrix.android.internal.database.helper.updateSenderData +import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.EventEntity @@ -41,7 +41,8 @@ internal interface PruneEventTask : Task { ) } -internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy) : PruneEventTask { +internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy, + private val timelineEventSenderVisitor: TimelineEventSenderVisitor) : PruneEventTask { override suspend fun execute(params: PruneEventTask.Params) { monarchy.awaitTransaction { realm -> @@ -65,12 +66,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() ?: return - val allowedKeys = computeAllowedKeys(eventToPrune.type) + val typeToPrune = eventToPrune.type + val stateKey = eventToPrune.stateKey + val allowedKeys = computeAllowedKeys(typeToPrune) if (allowedKeys.isNotEmpty()) { val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } eventToPrune.content = ContentMapper.map(prunedContent) } else { - when (eventToPrune.type) { + when (typeToPrune) { EventType.ENCRYPTED, EventType.MESSAGE -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") @@ -94,11 +97,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M // } } } - if (eventToPrune.type == EventType.STATE_ROOM_MEMBER) { + if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) { + timelineEventSenderVisitor.clear(roomId = eventToPrune.roomId, senderId = stateKey) val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId) - for (timelineEvent in timelineEventsToUpdate) { - timelineEvent.updateSenderData() - } + timelineEventSenderVisitor.visit(timelineEventsToUpdate) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 342a40052c..9773eff507 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 @@ -36,7 +36,8 @@ import javax.inject.Inject /** * Insert Chunk in DB, and eventually merge with existing chunk event */ -internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy) { +internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy, + private val timelineEventSenderVisitor: TimelineEventSenderVisitor) { /** *
    @@ -170,7 +171,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
                             for (stateEvent in receivedChunk.stateEvents) {
                                 roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked())
                             }
    -                        currentChunk.updateSenderDataFor(timelineEvents)
    +                        timelineEventSenderVisitor.visit(timelineEvents)
                         }
                     }
             return if (receivedChunk.events.isEmpty()) {
    @@ -191,11 +192,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
             // We always merge the bottom chunk into top chunk, so we are always merging backwards
             Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
             return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) {
    -            currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
    +            val events = currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
    +            timelineEventSenderVisitor.visit(events)
                 roomEntity.deleteOnCascade(otherChunk)
                 currentChunk
             } else {
    -            otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
    +            val events = otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
    +            timelineEventSenderVisitor.visit(events)
                 roomEntity.deleteOnCascade(currentChunk)
                 otherChunk
             }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    index f3c0cd9511..647635343e 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
    @@ -48,7 +48,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                        private val roomTagHandler: RoomTagHandler,
                                                        private val roomFullyReadHandler: RoomFullyReadHandler,
                                                        private val cryptoService: DefaultCryptoService,
    -                                                   private val roomMemberEventHandler: RoomMemberEventHandler) {
    +                                                   private val roomMemberEventHandler: RoomMemberEventHandler,
    +                                                   private val timelineEventSenderVisitor: TimelineEventSenderVisitor) {
     
         sealed class HandlingStrategy {
             data class JOINED(val data: Map) : HandlingStrategy()
    @@ -209,7 +210,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 }
                 roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
             }
    -        chunkEntity.updateSenderDataFor(timelineEvents)
    +        timelineEventSenderVisitor.visit(timelineEvents)
             return chunkEntity
         }
     
    
    From e32d242e382ee01472a7b34900c2621e9b74b4ab Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Tue, 31 Dec 2019 12:57:45 +0100
    Subject: [PATCH 051/105] Timeline: remove use of isUnlinked method as it slows
     down the insertion a lot
    
    ---
     .../database/helper/ChunkEntityHelper.kt      | 23 ++++++-------------
     .../helper/TimelineEventSenderVisitor.kt      |  6 +++--
     .../internal/database/model/ChunkEntity.kt    |  3 ++-
     .../database/query/ChunkEntityQueries.kt      |  8 ++++++-
     .../room/timeline/TokenChunkEventPersistor.kt | 12 ++++------
     .../internal/session/sync/RoomSyncHandler.kt  |  1 +
     .../riotx/features/home/HomeDetailFragment.kt |  1 -
     7 files changed, 25 insertions(+), 29 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 90236a4f98..4060e21102 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
    @@ -30,15 +30,6 @@ import im.vector.matrix.android.internal.session.room.timeline.PaginationDirecti
     import io.realm.Sort
     import io.realm.kotlin.createObject
     
    -// By default if a chunk is empty we consider it unlinked
    -internal fun ChunkEntity.isUnlinked(): Boolean {
    -    assertIsManaged()
    -    return timelineEvents.where()
    -            .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false)
    -            .findAll()
    -            .isEmpty()
    -}
    -
     internal fun ChunkEntity.deleteOnCascade() {
         assertIsManaged()
         this.timelineEvents.deleteAllFromRealm()
    @@ -49,9 +40,8 @@ internal fun ChunkEntity.merge(roomId: String,
                                    chunkToMerge: ChunkEntity,
                                    direction: PaginationDirection): List {
         assertIsManaged()
    -    val isChunkToMergeUnlinked = chunkToMerge.isUnlinked()
    -    val isCurrentChunkUnlinked = this.isUnlinked()
    -    val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked
    +    val isChunkToMergeUnlinked = chunkToMerge.isUnlinked
    +    val isCurrentChunkUnlinked = isUnlinked
     
         if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
             this.timelineEvents.forEach { it.root?.isUnlinked = false }
    @@ -69,15 +59,15 @@ internal fun ChunkEntity.merge(roomId: String,
         return eventsToMerge
                 .mapNotNull {
                     val event = it.root?.asDomain() ?: return@mapNotNull null
    -                add(roomId, event, direction, isUnlinked = isUnlinked)
    +                add(roomId, event, direction)
                 }
     }
     
     internal fun ChunkEntity.add(roomId: String,
                                  event: Event,
                                  direction: PaginationDirection,
    -                             stateIndexOffset: Int = 0,
    -                             isUnlinked: Boolean = false): TimelineEventEntity? {
    +                             stateIndexOffset: Int = 0
    +): TimelineEventEntity? {
         assertIsManaged()
         if (event.eventId != null && timelineEvents.find(event.eventId) != null) {
             return null
    @@ -102,6 +92,7 @@ internal fun ChunkEntity.add(roomId: String,
             }
         }
     
    +    val isUnlinked = isUnlinked
         val localId = TimelineEventEntity.nextId(realm)
         val eventId = event.eventId ?: ""
         val senderId = event.senderId ?: ""
    @@ -128,9 +119,9 @@ internal fun ChunkEntity.add(roomId: String,
     
         val rootEvent = event.toEntity(roomId).apply {
             this.stateIndex = currentStateIndex
    -        this.isUnlinked = isUnlinked
             this.displayIndex = currentDisplayIndex
             this.sendState = SendState.SYNCED
    +        this.isUnlinked = isUnlinked
         }
         val eventEntity = realm.createObject().also {
             it.localId = localId
    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
    index 2e8b02d6aa..983de3a50f 100644
    --- 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
    @@ -66,6 +66,9 @@ internal class TimelineEventSenderVisitor @Inject constructor() {
         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,
    @@ -96,7 +99,7 @@ internal class TimelineEventSenderVisitor @Inject constructor() {
             val stateIndex = root?.stateIndex ?: return result
             val senderId = root?.sender ?: return result
             val chunkEntity = chunk?.firstOrNull() ?: return result
    -        val isUnlinked = chunkEntity.isUnlinked()
    +        val isUnlinked = chunkEntity.isUnlinked
             var senderMembershipEvent: EventEntity?
             var senderRoomMemberContent: String?
             var senderRoomMemberPrevContent: String?
    @@ -142,5 +145,4 @@ internal class TimelineEventSenderVisitor @Inject constructor() {
             result.senderMembershipEventId = senderMembershipEvent?.eventId
             return result
         }
    -
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt
    index 577c391b3a..94d4a9043f 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt
    @@ -30,7 +30,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
                                     var backwardsDisplayIndex: Int? = null,
                                     var forwardsDisplayIndex: Int? = null,
                                     var backwardsStateIndex: Int? = null,
    -                                var forwardsStateIndex: Int? = null
    +                                var forwardsStateIndex: Int? = null,
    +                                var isUnlinked: Boolean = false
     ) : RealmObject() {
     
         fun identifier() = "${prevToken}_$nextToken"
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt
    index 69402ac1de..b8c058e667 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt
    @@ -57,9 +57,15 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
         return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
     }
     
    -internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
    +internal fun ChunkEntity.Companion.create(
    +        realm: Realm,
    +        prevToken: String?,
    +        nextToken: String?,
    +        isUnlinked: Boolean
    +): ChunkEntity {
         return realm.createObject().apply {
             this.prevToken = prevToken
             this.nextToken = nextToken
    +        this.isUnlinked = isUnlinked
         }
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
    index 9773eff507..87c59e832b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
    @@ -18,10 +18,6 @@ package im.vector.matrix.android.internal.session.room.timeline
     
     import com.zhuinden.monarchy.Monarchy
     import im.vector.matrix.android.internal.database.helper.*
    -import im.vector.matrix.android.internal.database.helper.add
    -import im.vector.matrix.android.internal.database.helper.addOrUpdate
    -import im.vector.matrix.android.internal.database.helper.addStateEvent
    -import im.vector.matrix.android.internal.database.helper.deleteOnCascade
     import im.vector.matrix.android.internal.database.model.ChunkEntity
     import im.vector.matrix.android.internal.database.model.RoomEntity
     import im.vector.matrix.android.internal.database.query.create
    @@ -136,14 +132,14 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
     
                         // The current chunk is the one we will keep all along the merge processChanges.
                         // We try to look for a chunk next to the token,
    -                    // otherwise we create a whole new one
    +                    // otherwise we create a whole new one which is unlinked (not live)
     
                         var currentChunk = if (direction == PaginationDirection.FORWARDS) {
                             prevChunk?.apply { this.nextToken = nextToken }
                         } else {
                             nextChunk?.apply { this.prevToken = prevToken }
                         }
    -                            ?: ChunkEntity.create(realm, prevToken, nextToken)
    +                            ?: ChunkEntity.create(realm, prevToken, nextToken, isUnlinked = true)
     
                         if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
                             Timber.v("Reach end of $roomId")
    @@ -151,7 +147,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
                         } else if (!shouldSkip) {
                             Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
                             val timelineEvents = receivedChunk.events.mapNotNull {
    -                            currentChunk.add(roomId, it, direction, isUnlinked = currentChunk.isUnlinked())
    +                            currentChunk.add(roomId, it, direction)
                             }
                             // Then we merge chunks if needed
                             if (currentChunk != prevChunk && prevChunk != null) {
    @@ -169,7 +165,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
                             }
                             roomEntity.addOrUpdate(currentChunk)
                             for (stateEvent in receivedChunk.stateEvents) {
    -                            roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked())
    +                            roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked)
                             }
                             timelineEventSenderVisitor.visit(timelineEvents)
                         }
    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 647635343e..488b9ce83d 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    @@ -190,6 +190,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             }
             lastChunk?.isLastForward = false
             chunkEntity.isLastForward = true
    +        chunkEntity.isUnlinked = false
     
             val timelineEvents = ArrayList(eventList.size)
             for (event in eventList) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    index 2913ce80fe..85f14e99a8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    @@ -198,5 +198,4 @@ class HomeDetailFragment @Inject constructor(
             RoomListDisplayMode.ROOMS  -> R.id.bottom_action_rooms
             else                       -> R.id.bottom_action_home
         }
    -
     }
    
    From 6b2703f6ce0eec44dacf360bec87fa9996d1ffc6 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 2 Jan 2020 15:05:17 +0100
    Subject: [PATCH 052/105] Device list is now on a dedicated Fragment New
     request to get info on the current device for
     VectorSettingsSecurityPrivacyFragment. The whole device list is only
     retrieved in the new Fragment
    
    ---
     .../api/extensions/MatrixSdkExtensions.kt     |  10 +-
     .../api/session/crypto/CryptoService.kt       |   3 +
     .../android/internal/crypto/CryptoModule.kt   |   3 +
     .../internal/crypto/DefaultCryptoService.kt   |  10 +
     .../android/internal/crypto/api/CryptoApi.kt  |   9 +-
     .../crypto/tasks/GetDeviceInfoTask.kt         |  37 ++
     .../im/vector/riotx/core/di/FragmentModule.kt |   8 +-
     .../features/settings/VectorPreferences.kt    |   2 -
     .../VectorSettingsSecurityPrivacyFragment.kt  | 375 +-----------------
     .../features/settings/devices/DeviceItem.kt   |  64 +++
     .../settings/devices/DevicesController.kt     |  88 ++++
     .../settings/devices/DevicesViewModel.kt      | 255 ++++++++++++
     .../devices/VectorSettingsDevicesFragment.kt  | 234 +++++++++++
     vector/src/main/res/layout/item_device.xml    |  44 ++
     vector/src/main/res/values/strings_riotX.xml  |   4 +
     .../xml/vector_settings_security_privacy.xml  |  24 +-
     16 files changed, 799 insertions(+), 371 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt
     create mode 100644 vector/src/main/res/layout/item_device.xml
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt
    index 685a522f60..bada3f86a1 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt
    @@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
             ?.chunked(4)
             ?.joinToString(separator = " ")
     
    -fun MutableList.sortByLastSeen() {
    -    sortWith(DatedObjectComparators.descComparator)
    +/* ==========================================================================================
    + * DeviceInfo
    + * ========================================================================================== */
    +
    +fun List.sortByLastSeen(): List {
    +    val list = toMutableList()
    +    list.sortWith(DatedObjectComparators.descComparator)
    +    return list
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
    index 706f89dfc9..986cbb698b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
    @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
     import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
     import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
     import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
    +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
     import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
     import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
     
    @@ -89,6 +90,8 @@ interface CryptoService {
     
         fun getDevicesList(callback: MatrixCallback)
     
    +    fun getDeviceInfo(deviceId: String, callback: MatrixCallback)
    +
         fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
     
         fun isRoomEncrypted(roomId: String): Boolean
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt
    index a12f6e40ce..fd180a93b0 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt
    @@ -123,6 +123,9 @@ internal abstract class CryptoModule {
         @Binds
         abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
     
    +    @Binds
    +    abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
    +
         @Binds
         abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask
     
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
    index 28a9fad35f..be8918dac7 100755
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
    @@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
     import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
     import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
     import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
    +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
     import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
     import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
     import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
    @@ -127,6 +128,7 @@ internal class DefaultCryptoService @Inject constructor(
             private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
             // Tasks
             private val getDevicesTask: GetDevicesTask,
    +        private val getDeviceInfoTask: GetDeviceInfoTask,
             private val setDeviceNameTask: SetDeviceNameTask,
             private val uploadKeysTask: UploadKeysTask,
             private val loadRoomMembersTask: LoadRoomMembersTask,
    @@ -199,6 +201,14 @@ internal class DefaultCryptoService @Inject constructor(
                     .executeBy(taskExecutor)
         }
     
    +    override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) {
    +        getDeviceInfoTask
    +                .configureWith(GetDeviceInfoTask.Params(deviceId)) {
    +                    this.callback = callback
    +                }
    +                .executeBy(taskExecutor)
    +    }
    +
         override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
             return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
         }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt
    index f4821f8ef3..b2e880c2f3 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt
    @@ -25,11 +25,18 @@ internal interface CryptoApi {
     
         /**
          * Get the devices list
    -     * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices
    +     * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices
          */
         @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices")
         fun getDevices(): Call
     
    +    /**
    +     * Get the device info by id
    +     * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid
    +     */
    +    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}")
    +    fun getDeviceInfo(@Path("deviceId") deviceId: String): Call
    +
         /**
          * Upload device and/or one-time keys.
          * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt
    new file mode 100644
    index 0000000000..f97e86a57d
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt
    @@ -0,0 +1,37 @@
    +/*
    + * Copyright 2019 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + * http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.matrix.android.internal.crypto.tasks
    +
    +import im.vector.matrix.android.internal.crypto.api.CryptoApi
    +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
    +import im.vector.matrix.android.internal.network.executeRequest
    +import im.vector.matrix.android.internal.task.Task
    +import javax.inject.Inject
    +
    +internal interface GetDeviceInfoTask : Task {
    +    data class Params(val deviceId: String)
    +}
    +
    +internal class DefaultGetDeviceInfoTask @Inject constructor(private val cryptoApi: CryptoApi)
    +    : GetDeviceInfoTask {
    +
    +    override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo {
    +        return executeRequest {
    +            apiCall = cryptoApi.getDeviceInfo(params.deviceId)
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    index c86436d56a..88af219c78 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    @@ -45,6 +45,7 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
     import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
     import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
     import im.vector.riotx.features.settings.*
    +import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
     import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
     import im.vector.riotx.features.settings.push.PushGatewaysFragment
     import im.vector.riotx.features.signout.soft.SoftLogoutFragment
    @@ -226,7 +227,12 @@ interface FragmentModule {
         @Binds
         @IntoMap
         @FragmentKey(VectorSettingsIgnoredUsersFragment::class)
    -    fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment
    +    fun  bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(VectorSettingsDevicesFragment::class)
    +    fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
     
         @Binds
         @IntoMap
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    index dd99488465..c3b07c7496 100755
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    @@ -62,8 +62,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY"
             const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
             const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY"
    -        const val SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
    -        const val SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY"
             const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY"
             const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY"
             const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index b7ec443ea0..72cc0884c9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -18,12 +18,8 @@ package im.vector.riotx.features.settings
     
     import android.annotation.SuppressLint
     import android.app.Activity
    -import android.content.DialogInterface
     import android.content.Intent
    -import android.graphics.Typeface
    -import android.view.KeyEvent
     import android.widget.Button
    -import android.widget.EditText
     import android.widget.TextView
     import androidx.appcompat.app.AlertDialog
     import androidx.core.view.isVisible
    @@ -33,30 +29,20 @@ import androidx.preference.SwitchPreference
     import com.google.android.material.textfield.TextInputEditText
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
    -import im.vector.matrix.android.api.extensions.sortByLastSeen
    -import im.vector.matrix.android.api.failure.Failure
    -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
     import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
    -import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
     import im.vector.riotx.R
     import im.vector.riotx.core.dialogs.ExportKeysDialog
     import im.vector.riotx.core.intent.ExternalIntentData
     import im.vector.riotx.core.intent.analyseIntent
     import im.vector.riotx.core.intent.getFilenameFromUri
     import im.vector.riotx.core.platform.SimpleTextWatcher
    -import im.vector.riotx.core.preference.ProgressBarPreference
     import im.vector.riotx.core.preference.VectorPreference
     import im.vector.riotx.core.preference.VectorPreferenceDivider
     import im.vector.riotx.core.utils.*
     import im.vector.riotx.features.crypto.keys.KeysExporter
     import im.vector.riotx.features.crypto.keys.KeysImporter
     import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
    -import timber.log.Timber
    -import java.text.DateFormat
    -import java.text.SimpleDateFormat
    -import java.util.Date
    -import java.util.Locale
     import javax.inject.Inject
     
     class VectorSettingsSecurityPrivacyFragment @Inject constructor(
    @@ -66,9 +52,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         override var titleRes = R.string.settings_security_and_privacy
         override val preferenceXmlRes = R.xml.vector_settings_security_privacy
     
    -    // used to avoid requesting to enter the password for each deletion
    -    private var mAccountPassword: String = ""
    -
         // devices: device IDs and device names
         private val mDevicesNameList: MutableList = mutableListOf()
     
    @@ -95,12 +78,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         private val mPushersSettingsCategory by lazy {
             findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!!
         }
    -    private val mDevicesListSettingsCategory by lazy {
    -        findPreference(VectorPreferences.SETTINGS_DEVICES_LIST_PREFERENCE_KEY)!!
    -    }
    -    private val mDevicesListSettingsCategoryDivider by lazy {
    -        findPreference(VectorPreferences.SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY)!!
    -    }
         private val cryptoInfoDeviceNamePreference by lazy {
             findPreference(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!!
         }
    @@ -129,13 +106,16 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             findPreference(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
         }
     
    +    override fun onResume() {
    +        super.onResume()
    +        // My device name may have been updated
    +        refreshMyDevice()
    +    }
    +
         override fun bindPref() {
             // Push target
             refreshPushersList()
     
    -        // Device list
    -        refreshDevicesList()
    -
             // Refresh Key Management section
             refreshKeysManagementSection()
     
    @@ -375,7 +355,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                 cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
     
                 cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    -                displayDeviceRenameDialog(aMyDeviceInfo)
    +                // TODO device can be rename only from the device list screen for the moment
    +                // displayDeviceRenameDialog(aMyDeviceInfo)
                     true
                 }
     
    @@ -428,342 +409,22 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         // devices list
         // ==============================================================================================================
     
    -    private fun removeDevicesPreference() {
    -        preferenceScreen.let {
    -            it.removePreference(mDevicesListSettingsCategory)
    -            it.removePreference(mDevicesListSettingsCategoryDivider)
    -        }
    -    }
    -
    -    /**
    -     * Force the refresh of the devices list.

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

    - * Each row correspond to a device ID and its corresponding device name. Clicking on the row - * display a dialog containing: the device ID, the device name and the "last seen" information. - * - * @param aDeviceInfoList the list of the devices - */ - private fun buildDevicesSettings(aDeviceInfoList: List) { - var preference: VectorPreference - var typeFaceHighlight: Int - var isNewList = true - val myDeviceId = session.sessionParams.credentials.deviceId - - if (aDeviceInfoList.size == mDevicesNameList.size) { - isNewList = !mDevicesNameList.containsAll(aDeviceInfoList) - } - - if (isNewList) { - var prefIndex = 0 - mDevicesNameList.clear() - mDevicesNameList.addAll(aDeviceInfoList) - - // sort before display: most recent first - mDevicesNameList.sortByLastSeen() - - // start from scratch: remove the displayed ones - mDevicesListSettingsCategory.removeAll() - - for (deviceInfo in mDevicesNameList) { - // set bold to distinguish current device ID - if (null != myDeviceId && myDeviceId == deviceInfo.deviceId) { - mMyDeviceInfo = deviceInfo - typeFaceHighlight = Typeface.BOLD - } else { - typeFaceHighlight = Typeface.NORMAL - } - - // add the edit text preference - preference = VectorPreference(requireActivity()).apply { - mTypeface = typeFaceHighlight - } - - if (null == deviceInfo.deviceId && null == deviceInfo.displayName) { - continue - } else { - if (null != deviceInfo.deviceId) { - preference.title = deviceInfo.deviceId - } - - // display name parameter can be null (new JSON API) - if (null != deviceInfo.displayName) { - preference.summary = deviceInfo.displayName - } - } - - preference.key = DEVICES_PREFERENCE_KEY_BASE + prefIndex - prefIndex++ - - // onClick handler: display device details dialog - preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - displayDeviceDetailsDialog(deviceInfo) - true - } - - mDevicesListSettingsCategory.addPreference(preference) - } - - refreshCryptographyPreference(mMyDeviceInfo) - } - } - - /** - * Display a dialog containing the device ID, the device name and the "last seen" information.<> - * This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog]) - * - * @param aDeviceInfo the device information - */ - private fun displayDeviceDetailsDialog(aDeviceInfo: DeviceInfo) { - activity?.let { - val builder = AlertDialog.Builder(it) - val inflater = it.layoutInflater - val layout = inflater.inflate(R.layout.dialog_device_details, null) - var textView = layout.findViewById(R.id.device_id) - - textView.text = aDeviceInfo.deviceId - - // device name - textView = layout.findViewById(R.id.device_name) - val displayName = if (aDeviceInfo.displayName.isNullOrEmpty()) LABEL_UNAVAILABLE_DATA else aDeviceInfo.displayName - textView.text = displayName - - // last seen info - textView = layout.findViewById(R.id.device_last_seen) - - val lastSeenIp = aDeviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" - - val lastSeenTime = aDeviceInfo.lastSeenTs?.let { ts -> - val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT) - val date = Date(ts) - - val time = dateFormatTime.format(date) - val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()) - - dateFormat.format(date) + ", " + time - } ?: "-" - - val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) - textView.text = lastSeenInfo - - // title & icon - builder.setTitle(R.string.devices_details_dialog_title) - .setIcon(android.R.drawable.ic_dialog_info) - .setView(layout) - .setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) } - - // disable the deletion for our own device - if (session.getMyDevice().deviceId != aDeviceInfo.deviceId) { - builder.setNegativeButton(R.string.delete) { _, _ -> deleteDevice(aDeviceInfo) } - } - - builder.setNeutralButton(R.string.cancel, null) - .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> - if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - dialog.cancel() - return@OnKeyListener true - } - false - }) - .show() - } - } - - /** - * Display an alert dialog to rename a device - * - * @param aDeviceInfoToRename device info - */ - private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) { - activity?.let { - val inflater = it.layoutInflater - val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) - - val input = layout.findViewById(R.id.edit_text) - input.setText(aDeviceInfoToRename.displayName) - - AlertDialog.Builder(it) - .setTitle(R.string.devices_details_device_name) - .setView(layout) - .setPositiveButton(R.string.ok) { _, _ -> - displayLoadingView() - - val newName = input.text.toString() - - session.setDeviceName(aDeviceInfoToRename.deviceId!!, newName, object : MatrixCallback { - override fun onSuccess(data: Unit) { - hideLoadingView() - - // search which preference is updated - val count = mDevicesListSettingsCategory.preferenceCount - - for (i in 0 until count) { - val pref = mDevicesListSettingsCategory.getPreference(i) - - if (aDeviceInfoToRename.deviceId == pref.title) { - pref.summary = newName - } - } - - // detect if the updated device is the current account one - if (cryptoInfoDeviceIdPreference.summary == aDeviceInfoToRename.deviceId) { - cryptoInfoDeviceNamePreference.summary = newName - } - - // Also change the display name in aDeviceInfoToRename, in case of multiple renaming - aDeviceInfoToRename.displayName = newName - } - - override fun onFailure(failure: Throwable) { - onCommonDone(failure.localizedMessage) - } - }) - } - .setNegativeButton(R.string.cancel, null) - .show() - } - } - - /** - * Try to delete a device. - * - * @param deviceInfo the device to delete - */ - private fun deleteDevice(deviceInfo: DeviceInfo) { - val deviceId = deviceInfo.deviceId - if (deviceId == null) { - Timber.e("## displayDeviceDeletionDialog(): sanity check failure") - return - } - - displayLoadingView() - session.deleteDevice(deviceId, object : MatrixCallback { - override fun onSuccess(data: Unit) { - hideLoadingView() - // force settings update - refreshDevicesList() - } - - override fun onFailure(failure: Throwable) { - var isPasswordRequestFound = false - - if (failure is Failure.RegistrationFlowError) { - // We only support LoginFlowTypes.PASSWORD - // Check if we can provide the user password - failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> - isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true - } - - if (isPasswordRequestFound) { - maybeShowDeleteDeviceWithPasswordDialog(deviceId, failure.registrationFlowResponse.session) - } - } - - if (!isPasswordRequestFound) { - // LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far... - onCommonDone(failure.localizedMessage) - } - } - }) - } - - /** - * Show a dialog to ask for user password, or use a previously entered password. - */ - private fun maybeShowDeleteDeviceWithPasswordDialog(deviceId: String, authSession: String?) { - if (mAccountPassword.isNotEmpty()) { - deleteDeviceWithPassword(deviceId, authSession, mAccountPassword) - } else { - activity?.let { - val inflater = it.layoutInflater - val layout = inflater.inflate(R.layout.dialog_device_delete, null) - val passwordEditText = layout.findViewById(R.id.delete_password) - - AlertDialog.Builder(it) - .setIcon(android.R.drawable.ic_dialog_alert) - .setTitle(R.string.devices_delete_dialog_title) - .setView(layout) - .setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ -> - if (passwordEditText.toString().isEmpty()) { - it.toast(R.string.error_empty_field_your_password) - return@OnClickListener - } - mAccountPassword = passwordEditText.text.toString() - deleteDeviceWithPassword(deviceId, authSession, mAccountPassword) - }) - .setNegativeButton(R.string.cancel) { _, _ -> - hideLoadingView() - } - .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> - if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - dialog.cancel() - hideLoadingView() - return@OnKeyListener true - } - false - }) - .show() - } - } - } - - private fun deleteDeviceWithPassword(deviceId: String, authSession: String?, accountPassword: String) { - session.deleteDeviceWithUserPassword(deviceId, authSession, accountPassword, object : MatrixCallback { - override fun onSuccess(data: Unit) { - hideLoadingView() - // force settings update - refreshDevicesList() - } - - override fun onFailure(failure: Throwable) { - // Password is maybe not good - onCommonDone(failure.localizedMessage) - mAccountPassword = "" - } - }) - } - // ============================================================================================================== // pushers list management // ============================================================================================================== @@ -860,6 +521,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE" // TODO i18n - private const val LABEL_UNAVAILABLE_DATA = "none" + const val LABEL_UNAVAILABLE_DATA = "none" } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt new file mode 100644 index 0000000000..201d4c88dd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.devices + +import android.graphics.Typeface +import android.view.View +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +/** + * A list item for Device. + */ +@EpoxyModelClass(layout = R.layout.item_device) +abstract class DeviceItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var deviceInfo: DeviceInfo + + @EpoxyAttribute + var bold = false + + @EpoxyAttribute + var itemClickAction: (() -> Unit)? = null + + override fun bind(holder: Holder) { + holder.root.setOnClickListener { itemClickAction?.invoke() } + + holder.displayNameText.text = deviceInfo.displayName ?: "" + holder.deviceIdText.text = deviceInfo.deviceId ?: "" + + if (bold) { + holder.displayNameText.setTypeface(null, Typeface.BOLD) + holder.deviceIdText.setTypeface(null, Typeface.BOLD) + } else { + holder.displayNameText.setTypeface(null, Typeface.NORMAL) + holder.deviceIdText.setTypeface(null, Typeface.NORMAL) + } + } + + class Holder : VectorEpoxyHolder() { + val root by bind(R.id.itemDeviceRoot) + val displayNameText by bind(R.id.itemDeviceDisplayName) + val deviceIdText by bind(R.id.itemDeviceId) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt new file mode 100644 index 0000000000..6fe25335df --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.devices + +import com.airbnb.epoxy.EpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.extensions.sortByLastSeen +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import javax.inject.Inject + +class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter) : EpoxyController() { + + var callback: Callback? = null + private var viewState: DevicesViewState? = null + + init { + requestModelBuild() + } + + fun update(viewState: DevicesViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val nonNullViewState = viewState ?: return + buildDevicesModels(nonNullViewState) + } + + private fun buildDevicesModels(state: DevicesViewState) { + when (val devices = state.devices) { + is Loading, + is Uninitialized -> + loadingItem { + id("loading") + } + is Fail -> + errorWithRetryItem { + id("error") + text(errorFormatter.toHumanReadable(devices.error)) + listener { callback?.retry() } + } + is Success -> + // Build the devices portion of the settings. + // Each row correspond to a device ID and its corresponding device name. Clicking on the row + // display a dialog containing: the device ID, the device name and the "last seen" information. + devices() + // sort before display: most recent first + .sortByLastSeen() + .forEachIndexed { idx, deviceInfo -> + val isCurrentDevice = deviceInfo.deviceId == state.myDeviceId + deviceItem { + id("device$idx") + deviceInfo(deviceInfo) + bold(isCurrentDevice) + itemClickAction { callback?.onDeviceClicked(deviceInfo, isCurrentDevice) } + } + } + } + } + + interface Callback { + fun retry() + fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) + } +} + + diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt new file mode 100644 index 0000000000..c06912cdeb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.devices + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse +import im.vector.riotx.core.extensions.postLiveEvent +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.core.utils.LiveEvent +import timber.log.Timber + +data class DevicesViewState( + val myDeviceId: String = "", + val devices: Async> = Uninitialized, + val request: Async = Uninitialized +) : MvRxState + +sealed class DevicesAction : VectorViewModelAction { + object Retry : DevicesAction() + data class Delete(val deviceInfo: DeviceInfo) : DevicesAction() + data class Password(val password: String) : DevicesAction() + data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction() +} + +class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DevicesViewState): DevicesViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: DevicesViewState): DevicesViewModel? { + val fragment: VectorSettingsDevicesFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.devicesViewModelFactory.create(state) + } + } + + // temp storage when we ask for the user password + private var _currentDeviceId: String? = null + private var _currentSession: String? = null + + private val _requestPasswordLiveData = MutableLiveData>() + val requestPasswordLiveData: LiveData> + get() = _requestPasswordLiveData + + init { + refreshDevicesList() + } + + /** + * Force the refresh of the devices list. + * The devices list is the list of the devices where the user as logged in. + * It can be any mobile device, as any browser. + */ + private fun refreshDevicesList() { + if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) { + setState { + copy( + devices = Loading() + ) + } + + session.getDevicesList(object : MatrixCallback { + override fun onSuccess(data: DevicesListResponse) { + setState { + copy( + myDeviceId = session.sessionParams.credentials.deviceId ?: "", + devices = Success(data.devices.orEmpty()) + ) + } + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + devices = Fail(failure) + ) + } + } + }) + } else { + // Should not happen + } + } + + override fun handle(action: DevicesAction) { + return when (action) { + is DevicesAction.Retry -> refreshDevicesList() + is DevicesAction.Delete -> handleDelete(action) + is DevicesAction.Password -> handlePassword(action) + is DevicesAction.Rename -> handleRename(action) + } + } + + private fun handleRename(action: DevicesAction.Rename) { + session.setDeviceName(action.deviceInfo.deviceId!!, action.newName, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { + copy( + request = Success(data) + ) + } + // force settings update + refreshDevicesList() + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + request = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + }) + } + + /** + * Try to delete a device. + */ + private fun handleDelete(action: DevicesAction.Delete) { + val deviceId = action.deviceInfo.deviceId + if (deviceId == null) { + Timber.e("## handleDelete(): sanity check failure") + return + } + + setState { + copy( + request = Loading() + ) + } + + session.deleteDevice(deviceId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + var isPasswordRequestFound = false + + if (failure is Failure.RegistrationFlowError) { + // We only support LoginFlowTypes.PASSWORD + // Check if we can provide the user password + failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> + isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true + } + + if (isPasswordRequestFound) { + _currentDeviceId = deviceId + _currentSession = failure.registrationFlowResponse.session + + setState { + copy( + request = Success(Unit) + ) + } + + _requestPasswordLiveData.postLiveEvent(Unit) + } + } + + if (!isPasswordRequestFound) { + // LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far... + setState { + copy( + request = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + } + + override fun onSuccess(data: Unit) { + setState { + copy( + request = Success(data) + ) + } + // force settings update + refreshDevicesList() + } + }) + } + + private fun handlePassword(action: DevicesAction.Password) { + val currentDeviceId = _currentDeviceId + if (currentDeviceId.isNullOrBlank()) { + // Abort + return + } + + setState { + copy( + request = Loading() + ) + } + + session.deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback { + override fun onSuccess(data: Unit) { + _currentDeviceId = null + _currentSession = null + + setState { + copy( + request = Success(data) + ) + } + // force settings update + refreshDevicesList() + } + + override fun onFailure(failure: Throwable) { + _currentDeviceId = null + _currentSession = null + + // Password is maybe not good + setState { + copy( + request = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + }) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt new file mode 100644 index 0000000000..96a6d11e06 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -0,0 +1,234 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.devices + +import android.content.DialogInterface +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.widget.EditText +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Inject + +/** + * Display the list of the user's device + */ +class VectorSettingsDevicesFragment @Inject constructor( + val devicesViewModelFactory: DevicesViewModel.Factory, + private val devicesController: DevicesController +) : VectorBaseFragment(), DevicesController.Callback { + + // used to avoid requesting to enter the password for each deletion + private var mAccountPassword: String = "" + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + private val devicesViewModel: DevicesViewModel by fragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + waiting_view_status_text.setText(R.string.please_wait) + waiting_view_status_text.isVisible = true + devicesController.callback = this + recyclerView.configureWith(devicesController) + devicesViewModel.requestErrorLiveData.observeEvent(this) { + displayErrorDialog(it) + // Password is maybe not good, for safety measure, reset it here + mAccountPassword = "" + } + devicesViewModel.requestPasswordLiveData.observeEvent(this) { + maybeShowDeleteDeviceWithPasswordDialog() + } + } + + override fun onDestroyView() { + devicesController.callback = null + recyclerView.cleanup() + super.onDestroyView() + } + + override fun onResume() { + super.onResume() + + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_devices_list) + } + + private fun displayErrorDialog(throwable: Throwable) { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(throwable)) + .setPositiveButton(R.string.ok, null) + .show() + } + + /** + * Display a dialog containing the device ID, the device name and the "last seen" information.<> + * This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog]) + * + * @param deviceInfo the device information + * @param isCurrentDevice true if this is the current device + */ + override fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) { + val builder = AlertDialog.Builder(requireActivity()) + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_device_details, null) + var textView = layout.findViewById(R.id.device_id) + + textView.text = deviceInfo.deviceId + + // device name + textView = layout.findViewById(R.id.device_name) + val displayName = if (deviceInfo.displayName.isNullOrEmpty()) VectorSettingsSecurityPrivacyFragment.LABEL_UNAVAILABLE_DATA else deviceInfo.displayName + textView.text = displayName + + // last seen info + textView = layout.findViewById(R.id.device_last_seen) + + val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" + + val lastSeenTime = deviceInfo.lastSeenTs?.let { ts -> + val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT) + val date = Date(ts) + + val time = dateFormatTime.format(date) + val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()) + + dateFormat.format(date) + ", " + time + } ?: "-" + + val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) + textView.text = lastSeenInfo + + // title & icon + builder.setTitle(R.string.devices_details_dialog_title) + .setIcon(android.R.drawable.ic_dialog_info) + .setView(layout) + .setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(deviceInfo) } + + // disable the deletion for our own device + if (!isCurrentDevice) { + builder.setNegativeButton(R.string.delete) { _, _ -> devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) } + } + + builder.setNeutralButton(R.string.cancel, null) + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) + .show() + } + + override fun retry() { + devicesViewModel.handle(DevicesAction.Retry) + } + + /** + * Display an alert dialog to rename a device + * + * @param aDeviceInfoToRename device info + */ + private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) { + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) + + val input = layout.findViewById(R.id.edit_text) + input.setText(aDeviceInfoToRename.displayName) + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.devices_details_device_name) + .setView(layout) + .setPositiveButton(R.string.ok) { _, _ -> + val newName = input.text.toString() + + devicesViewModel.handle(DevicesAction.Rename(aDeviceInfoToRename, newName)) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + /** + * Show a dialog to ask for user password, or use a previously entered password. + */ + private fun maybeShowDeleteDeviceWithPasswordDialog() { + if (mAccountPassword.isNotEmpty()) { + devicesViewModel.handle(DevicesAction.Password(mAccountPassword)) + } else { + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_device_delete, null) + val passwordEditText = layout.findViewById(R.id.delete_password) + + AlertDialog.Builder(requireActivity()) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.devices_delete_dialog_title) + .setView(layout) + .setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ -> + if (passwordEditText.toString().isEmpty()) { + requireActivity().toast(R.string.error_empty_field_your_password) + return@OnClickListener + } + mAccountPassword = passwordEditText.text.toString() + devicesViewModel.handle(DevicesAction.Password(mAccountPassword)) + }) + .setNegativeButton(R.string.cancel, null) + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) + .show() + } + } + + override fun invalidate() = withState(devicesViewModel) { state -> + devicesController.update(state) + + handleRequestStatus(state.request) + } + + private fun handleRequestStatus(unIgnoreRequest: Async) { + when (unIgnoreRequest) { + is Loading -> waiting_view.isVisible = true + else -> waiting_view.isVisible = false + } + } +} diff --git a/vector/src/main/res/layout/item_device.xml b/vector/src/main/res/layout/item_device.xml new file mode 100644 index 0000000000..59b7ba92c0 --- /dev/null +++ b/vector/src/main/res/layout/item_device.xml @@ -0,0 +1,44 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8ee63bc628..754b1b6631 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -5,4 +5,8 @@ Initial Sync… + + + See all my devices + diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index 9e88da34a1..49d782479d 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -1,5 +1,6 @@ - + + + + + + + + + + @@ -50,13 +65,6 @@ - - - - - From 8dff196716a32683eebe32fe797d3b6146b7f3c5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 15:42:42 +0100 Subject: [PATCH 053/105] Device list: remove the detail dialog: handle the actions directly in the list --- .../features/settings/devices/DeviceItem.kt | 61 +++++++++++++-- .../settings/devices/DevicesController.kt | 13 ++-- .../settings/devices/DevicesViewModel.kt | 21 ++++- .../devices/VectorSettingsDevicesFragment.kt | 64 +++------------ .../main/res/layout/dialog_device_details.xml | 66 ---------------- vector/src/main/res/layout/item_device.xml | 78 ++++++++++++++++--- 6 files changed, 153 insertions(+), 150 deletions(-) delete mode 100644 vector/src/main/res/layout/dialog_device_details.xml diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt index 201d4c88dd..b6c84ade9a 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt @@ -18,13 +18,18 @@ package im.vector.riotx.features.settings.devices import android.graphics.Typeface import android.view.View +import android.view.ViewGroup import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* /** * A list item for Device. @@ -36,29 +41,69 @@ abstract class DeviceItem : VectorEpoxyModel() { lateinit var deviceInfo: DeviceInfo @EpoxyAttribute - var bold = false + var currentDevice = false + + @EpoxyAttribute + var buttonsVisible = false @EpoxyAttribute var itemClickAction: (() -> Unit)? = null + @EpoxyAttribute + var renameClickAction: (() -> Unit)? = null + + @EpoxyAttribute + var deleteClickAction: (() -> Unit)? = null + override fun bind(holder: Holder) { holder.root.setOnClickListener { itemClickAction?.invoke() } holder.displayNameText.text = deviceInfo.displayName ?: "" holder.deviceIdText.text = deviceInfo.deviceId ?: "" - if (bold) { - holder.displayNameText.setTypeface(null, Typeface.BOLD) - holder.deviceIdText.setTypeface(null, Typeface.BOLD) - } else { - holder.displayNameText.setTypeface(null, Typeface.NORMAL) - holder.deviceIdText.setTypeface(null, Typeface.NORMAL) + val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" + + val lastSeenTime = deviceInfo.lastSeenTs?.let { ts -> + val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT) + val date = Date(ts) + + val time = dateFormatTime.format(date) + val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()) + + dateFormat.format(date) + ", " + time + } ?: "-" + + holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) + + listOf( + holder.displayNameLabelText, + holder.displayNameText, + holder.deviceIdLabelText, + holder.deviceIdText, + holder.deviceLastSeenLabelText, + holder.deviceLastSeenText + ).map { + it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL) } + + holder.buttonDelete.isVisible = !currentDevice + + holder.buttons.isVisible = buttonsVisible + + holder.buttonRename.setOnClickListener { renameClickAction?.invoke() } + holder.buttonDelete.setOnClickListener { deleteClickAction?.invoke() } } class Holder : VectorEpoxyHolder() { - val root by bind(R.id.itemDeviceRoot) + val root by bind(R.id.itemDeviceRoot) + val displayNameLabelText by bind(R.id.itemDeviceDisplayNameLabel) val displayNameText by bind(R.id.itemDeviceDisplayName) + val deviceIdLabelText by bind(R.id.itemDeviceIdLabel) val deviceIdText by bind(R.id.itemDeviceId) + val deviceLastSeenLabelText by bind(R.id.itemDeviceLastSeenLabel) + val deviceLastSeenText by bind(R.id.itemDeviceLastSeen) + val buttons by bind(R.id.itemDeviceButtons) + val buttonDelete by bind(R.id.itemDeviceDelete) + val buttonRename by bind(R.id.itemDeviceRename) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt index 6fe25335df..db61db429b 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -72,8 +72,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor deviceItem { id("device$idx") deviceInfo(deviceInfo) - bold(isCurrentDevice) - itemClickAction { callback?.onDeviceClicked(deviceInfo, isCurrentDevice) } + currentDevice(isCurrentDevice) + buttonsVisible(deviceInfo.deviceId == state.currentExpandedDeviceId) + itemClickAction { callback?.onDeviceClicked(deviceInfo) } + renameClickAction { callback?.onRenameDevice(deviceInfo) } + deleteClickAction { callback?.onDeleteDevice(deviceInfo) } } } } @@ -81,8 +84,8 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor interface Callback { fun retry() - fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) + fun onDeviceClicked(deviceInfo: DeviceInfo) + fun onRenameDevice(deviceInfo: DeviceInfo) + fun onDeleteDevice(deviceInfo: DeviceInfo) } } - - diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt index c06912cdeb..1fc56e76fd 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -36,6 +36,7 @@ import timber.log.Timber data class DevicesViewState( val myDeviceId: String = "", val devices: Async> = Uninitialized, + val currentExpandedDeviceId: String? = null, val request: Async = Uninitialized ) : MvRxState @@ -44,6 +45,7 @@ sealed class DevicesAction : VectorViewModelAction { data class Delete(val deviceInfo: DeviceInfo) : DevicesAction() data class Password(val password: String) : DevicesAction() data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction() + data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction() } class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState, @@ -114,10 +116,21 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic override fun handle(action: DevicesAction) { return when (action) { - is DevicesAction.Retry -> refreshDevicesList() - is DevicesAction.Delete -> handleDelete(action) - is DevicesAction.Password -> handlePassword(action) - is DevicesAction.Rename -> handleRename(action) + is DevicesAction.Retry -> refreshDevicesList() + is DevicesAction.Delete -> handleDelete(action) + is DevicesAction.Password -> handlePassword(action) + is DevicesAction.Rename -> handleRename(action) + is DevicesAction.ToggleDevice -> handleToggleDevice(action) + } + } + + private fun handleToggleDevice(action: DevicesAction.ToggleDevice) { + withState { + setState { + copy( + currentExpandedDeviceId = if (it.currentExpandedDeviceId == action.deviceInfo.deviceId) null else action.deviceInfo.deviceId + ) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt index 96a6d11e06..4168e3eeb7 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -21,7 +21,6 @@ import android.os.Bundle import android.view.KeyEvent import android.view.View import android.widget.EditText -import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import com.airbnb.mvrx.Async @@ -36,12 +35,8 @@ import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.toast -import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* import javax.inject.Inject /** @@ -97,63 +92,22 @@ class VectorSettingsDevicesFragment @Inject constructor( } /** - * Display a dialog containing the device ID, the device name and the "last seen" information.<> + * Display a dialog containing the device ID, the device name and the "last seen" information. * This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog]) * * @param deviceInfo the device information * @param isCurrentDevice true if this is the current device */ - override fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) { - val builder = AlertDialog.Builder(requireActivity()) - val inflater = requireActivity().layoutInflater - val layout = inflater.inflate(R.layout.dialog_device_details, null) - var textView = layout.findViewById(R.id.device_id) + override fun onDeviceClicked(deviceInfo: DeviceInfo) { + devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo)) + } - textView.text = deviceInfo.deviceId + override fun onDeleteDevice(deviceInfo: DeviceInfo) { + devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) + } - // device name - textView = layout.findViewById(R.id.device_name) - val displayName = if (deviceInfo.displayName.isNullOrEmpty()) VectorSettingsSecurityPrivacyFragment.LABEL_UNAVAILABLE_DATA else deviceInfo.displayName - textView.text = displayName - - // last seen info - textView = layout.findViewById(R.id.device_last_seen) - - val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" - - val lastSeenTime = deviceInfo.lastSeenTs?.let { ts -> - val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT) - val date = Date(ts) - - val time = dateFormatTime.format(date) - val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()) - - dateFormat.format(date) + ", " + time - } ?: "-" - - val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) - textView.text = lastSeenInfo - - // title & icon - builder.setTitle(R.string.devices_details_dialog_title) - .setIcon(android.R.drawable.ic_dialog_info) - .setView(layout) - .setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(deviceInfo) } - - // disable the deletion for our own device - if (!isCurrentDevice) { - builder.setNegativeButton(R.string.delete) { _, _ -> devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) } - } - - builder.setNeutralButton(R.string.cancel, null) - .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> - if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - dialog.cancel() - return@OnKeyListener true - } - false - }) - .show() + override fun onRenameDevice(deviceInfo: DeviceInfo) { + displayDeviceRenameDialog(deviceInfo) } override fun retry() { diff --git a/vector/src/main/res/layout/dialog_device_details.xml b/vector/src/main/res/layout/dialog_device_details.xml deleted file mode 100644 index b3b5c5aff7..0000000000 --- a/vector/src/main/res/layout/dialog_device_details.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_device.xml b/vector/src/main/res/layout/item_device.xml index 59b7ba92c0..bebaf156d9 100644 --- a/vector/src/main/res/layout/item_device.xml +++ b/vector/src/main/res/layout/item_device.xml @@ -1,6 +1,5 @@ + + + + + + + + + + + + + + + + \ No newline at end of file From 7d744f7d7faa18683e165ca541a8c488b38ef211 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 16:01:47 +0100 Subject: [PATCH 054/105] Developer mode: UI And some cleanup --- .../VectorSettingsDeveloperModeFragment.kt | 29 +++++++++++++ vector/src/main/res/values/strings_riotX.xml | 3 +- .../xml/vector_settings_developer_mode.xml | 42 +++++++++++++++++++ .../src/main/res/xml/vector_settings_labs.xml | 12 ------ .../res/xml/vector_settings_notifications.xml | 15 ------- .../src/main/res/xml/vector_settings_root.xml | 14 +++---- 6 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt create mode 100644 vector/src/main/res/xml/vector_settings_developer_mode.xml diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt new file mode 100644 index 0000000000..00f71192bf --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings + +import im.vector.riotx.R + +class VectorSettingsDeveloperModeFragment : VectorSettingsBaseFragment() { + + override var titleRes = R.string.settings_developer_mode + override val preferenceXmlRes = R.xml.vector_settings_developer_mode + + override fun bindPref() { + // Nothing to do + } +} diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 754b1b6631..8c8e36be16 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -6,7 +6,8 @@ Initial Sync… - See all my devices + Developer mode + The developer mode activates hidden features and may also make the application less stable. For developers only! diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml new file mode 100644 index 0000000000..1f81e2e659 --- /dev/null +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index e9e5e27198..2661568f77 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -34,24 +34,12 @@ - - - - - \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml index 26f204c17c..4cfbaa0d7b 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -75,19 +75,4 @@ - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index 894784767a..0a59570050 100644 --- a/vector/src/main/res/xml/vector_settings_root.xml +++ b/vector/src/main/res/xml/vector_settings_root.xml @@ -3,58 +3,54 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + + From 703a1a034d00a06132cff3cb429e333820d3775e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 16:10:55 +0100 Subject: [PATCH 055/105] Developer mode: hide show (decrypted) source actions --- CHANGES.md | 2 ++ .../timeline/action/MessageActionsViewModel.kt | 17 +++++++++++------ .../features/settings/VectorPreferences.kt | 9 +++++++-- .../res/xml/vector_settings_developer_mode.xml | 8 ++++---- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e2cd2915b5..35f4a23804 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ Improvements 🙌: - The initial sync is now handled by a foreground service - Render aliases and canonical alias change in the timeline - Fix autocompletion issues and add support for rooms and groups + - Introduce developer mode in the settings (#796) + - Improve devices list screen Other changes: - 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 d537b66ec3..d08a891081 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 @@ -42,6 +42,7 @@ import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventForm import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.VectorHtmlCompressor +import im.vector.riotx.features.settings.VectorPreferences import java.text.SimpleDateFormat import java.util.* @@ -86,7 +87,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private val htmlCompressor: VectorHtmlCompressor, private val session: Session, private val noticeEventFormatter: NoticeEventFormatter, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences ) : VectorViewModel(initialState) { private val eventId = initialState.eventId @@ -268,12 +270,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent())) - if (event.isEncrypted()) { - val decryptedContent = event.root.toClearContentStringWithIndent() - ?: stringProvider.getString(R.string.encryption_information_decryption_error) - add(EventSharedAction.ViewDecryptedSource(decryptedContent)) + if (vectorPreferences.developerMode()) { + add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent())) + if (event.isEncrypted()) { + val decryptedContent = event.root.toClearContentStringWithIndent() + ?: stringProvider.getString(R.string.encryption_information_decryption_error) + add(EventSharedAction.ViewDecryptedSource(decryptedContent)) + } } + add(EventSharedAction.CopyPermalink(eventId)) if (session.myUserId != event.root.senderId) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index c3b07c7496..63b7c60bef 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -147,6 +147,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS" + private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY" private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" @@ -245,8 +246,12 @@ class VectorPreferences @Inject constructor(private val context: Context) { } } + fun developerMode(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY, false) + } + fun shouldShowHiddenEvents(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false) + return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false) } fun swipeToReplyIsEnabled(): Boolean { @@ -254,7 +259,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { } fun labAllowedExtendedLogging(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false) + return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false) } /** diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml index 1f81e2e659..8ceb03f323 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -4,19 +4,19 @@ @@ -24,7 +24,7 @@ Date: Thu, 2 Jan 2020 16:24:31 +0100 Subject: [PATCH 056/105] Hide non working settings (#751) --- CHANGES.md | 1 + vector/src/main/res/values/strings_riotX.xml | 1 + .../main/res/xml/vector_settings_general.xml | 9 ++-- .../res/xml/vector_settings_preferences.xml | 41 ++++++++++++------- .../xml/vector_settings_security_privacy.xml | 20 ++++++--- 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 35f4a23804..67b430e79b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ Other changes: Bugfix 🐛: - Fix avatar image disappearing (#777) - Fix read marker banner when permalink + - Hide non working settings (#751) Translations 🗣: - diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8c8e36be16..d0019bfca5 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -9,5 +9,6 @@ See all my devices Developer mode The developer mode activates hidden features and may also make the application less stable. For developers only! + Rageshake diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index d1ffe5bcf1..cd94da2d15 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -74,11 +74,12 @@ + tools:summary="https://homeserver.org" /> - + + android:title="@string/settings_deactivate_account_section" + app:isPreferenceVisible="@bool/false_not_implemented"> + android:title="@string/settings_inline_url_preview" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_send_typing_notifs" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_always_show_timestamps" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_12_24_timestamps" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_read_receipts" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_join_leave_messages" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_avatar_display_name_changes_messages" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_vibrate_on_mention" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_send_message_with_enter" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_info_area_show" + app:isPreferenceVisible="@bool/false_not_implemented" /> - + + android:title="@string/settings_home_display" + app:isPreferenceVisible="@bool/false_not_implemented"> - + - + + android:title="@string/encryption_never_send_to_unverified_devices_title" + app:isPreferenceVisible="@bool/false_not_implemented" /> @@ -36,7 +37,7 @@ + app:fragment="im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment" /> @@ -63,11 +64,14 @@ - + + android:title="@string/settings_analytics" + app:isPreferenceVisible="@bool/false_not_implemented"> + + + + + + - + \ No newline at end of file From 5c26f66523ad5bd72677215a65d6a045604f13d4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 17:42:44 +0100 Subject: [PATCH 057/105] Rageshake: settings for sensitivity --- .../riotx/core/platform/VectorBaseActivity.kt | 11 ++- .../riotx/features/rageshake/RageShake.kt | 85 +++++++------------ .../features/settings/VectorPreferences.kt | 11 +++ .../VectorSettingsDeveloperModeFragment.kt | 51 ++++++++++- vector/src/main/res/values/strings_riotX.xml | 4 + .../xml/vector_settings_developer_mode.xml | 85 ++++++++++++------- .../xml/vector_settings_security_privacy.xml | 9 -- 7 files changed, 158 insertions(+), 98 deletions(-) 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 f70aed9393..f4e3631b8a 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -54,6 +54,7 @@ import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.session.SessionListener +import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.receivers.DebugReceiver @@ -88,9 +89,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { private lateinit var configurationViewModel: ConfigurationViewModel private lateinit var sessionListener: SessionListener protected lateinit var bugReporter: BugReporter - private lateinit var rageShake: RageShake + lateinit var rageShake: RageShake + private set protected lateinit var navigator: Navigator private lateinit var activeSessionHolder: ActiveSessionHolder + private lateinit var vectorPreferences: VectorPreferences // Filter for multiple invalid token error private var mainActivityStarted = false @@ -135,7 +138,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } override fun onCreate(savedInstanceState: Bundle?) { - screenComponent = DaggerScreenComponent.factory().create(getVectorComponent(), this) + val vectorComponent = getVectorComponent() + screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this) val timeForInjection = measureTimeMillis { injectWith(screenComponent) } @@ -150,6 +154,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { rageShake = screenComponent.rageShake() navigator = screenComponent.navigator() activeSessionHolder = screenComponent.activeSessionHolder() + vectorPreferences = vectorComponent.vectorPreferences() configurationViewModel.activityRestarter.observe(this, Observer { if (!it.hasBeenHandled) { // Recreate the Activity because configuration has changed @@ -226,7 +231,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { configurationViewModel.onActivityResumed() - if (this !is BugReportActivity) { + if (this !is BugReportActivity && vectorPreferences.useRageshake()) { rageShake.start() } diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt index 39749be8c2..bbd1090d98 100644 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt @@ -19,33 +19,28 @@ package im.vector.riotx.features.rageshake import android.content.Context import android.hardware.Sensor import android.hardware.SensorManager -import android.preference.PreferenceManager import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.edit import com.squareup.seismic.ShakeDetector import im.vector.riotx.R +import im.vector.riotx.features.settings.VectorPreferences import javax.inject.Inject class RageShake @Inject constructor(private val activity: AppCompatActivity, - private val bugReporter: BugReporter) : ShakeDetector.Listener { + private val bugReporter: BugReporter, + private val vectorPreferences: VectorPreferences) : ShakeDetector.Listener { private var shakeDetector: ShakeDetector? = null private var dialogDisplayed = false + var interceptor: (() -> Unit)? = null + fun start() { - if (!isEnable(activity)) { - return - } - - val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager - - if (sensorManager == null) { - return - } + val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager ?: return shakeDetector = ShakeDetector(this).apply { + setSensitivity(vectorPreferences.getRageshakeSensitivity()) start(sensorManager) } } @@ -54,52 +49,41 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, shakeDetector?.stop() } - /** - * Enable the feature, and start it - */ - fun enable() { - PreferenceManager.getDefaultSharedPreferences(activity).edit { - putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true) - } - - start() - } - - /** - * Disable the feature, and stop it - */ - fun disable() { - PreferenceManager.getDefaultSharedPreferences(activity).edit { - putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, false) - } - - stop() + fun setSensitivity(sensitivity: Int) { + shakeDetector?.setSensitivity(sensitivity) } override fun hearShake() { - if (dialogDisplayed) { - // Filtered! - return + val i = interceptor + if (i != null) { + i.invoke() + } else { + if (dialogDisplayed) { + // Filtered! + return + } + + dialogDisplayed = true + + AlertDialog.Builder(activity) + .setMessage(R.string.send_bug_report_alert_message) + .setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() } + .setNeutralButton(R.string.settings) { _, _ -> openSettings() } + .setOnDismissListener { dialogDisplayed = false } + .setNegativeButton(R.string.no, null) + .show() } - - dialogDisplayed = true - - AlertDialog.Builder(activity) - .setMessage(R.string.send_bug_report_alert_message) - .setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() } - .setNeutralButton(R.string.disable) { _, _ -> disable() } - .setOnDismissListener { dialogDisplayed = false } - .setNegativeButton(R.string.no, null) - .show() } private fun openBugReportScreen() { bugReporter.openBugReportScreen(activity) } - companion object { - private const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" + private fun openSettings() { + // TODO + } + companion object { /** * Check if the feature is available */ @@ -107,12 +91,5 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, return (context.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager) ?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null } - - /** - * Check if the feature is enable (enabled by default) - */ - private fun isEnable(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true) - } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 63b7c60bef..a86c3625ce 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -23,6 +23,7 @@ import android.net.Uri import android.provider.MediaStore import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.squareup.seismic.ShakeDetector import im.vector.riotx.R import im.vector.riotx.features.homeserver.ServerUrlsRepository import im.vector.riotx.features.themes.ThemeUtils @@ -153,7 +154,10 @@ class VectorPreferences @Inject constructor(private val context: Context) { // analytics const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" + + // Rageshake const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" + const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY" // other const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY" @@ -732,6 +736,13 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true) } + /** + * Get the rage shake sensitivity. + */ + fun getRageshakeSensitivity(): Int { + return defaultPrefs.getInt(SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY, ShakeDetector.SENSITIVITY_MEDIUM) + } + /** * Update the rage shake status. * diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt index 00f71192bf..f700759fd0 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt @@ -16,14 +16,63 @@ package im.vector.riotx.features.settings +import androidx.preference.Preference +import androidx.preference.SeekBarPreference import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.preference.VectorSwitchPreference +import im.vector.riotx.features.rageshake.RageShake class VectorSettingsDeveloperModeFragment : VectorSettingsBaseFragment() { override var titleRes = R.string.settings_developer_mode override val preferenceXmlRes = R.xml.vector_settings_developer_mode + private var rageshake: RageShake? = null + + override fun onResume() { + super.onResume() + + rageshake = (activity as? VectorBaseActivity)?.rageShake + rageshake?.interceptor = { + (activity as? VectorBaseActivity)?.showSnackbar(getString(R.string.rageshake_detected)) + } + } + + override fun onPause() { + super.onPause() + rageshake?.interceptor = null + rageshake = null + } + override fun bindPref() { - // Nothing to do + val isRageShakeAvailable = RageShake.isAvailable(requireContext()) + + if (isRageShakeAvailable) { + findPreference(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!! + .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + + if (newValue as? Boolean == true) { + rageshake?.start() + } else { + rageshake?.stop() + } + + true + } + + findPreference(VectorPreferences.SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY)!! + .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + (activity as? VectorBaseActivity)?.let { + val newValueAsInt = newValue as? Int ?: return@OnPreferenceChangeListener true + + rageshake?.setSensitivity(newValueAsInt) + } + + true + } + } else { + findPreference("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false + } } } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index d0019bfca5..1134b4be0f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -10,5 +10,9 @@ Developer mode The developer mode activates hidden features and may also make the application less stable. For developers only! Rageshake + Detection threshold + Shake your phone to test the detection threshold + Shake detected! + Settings diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml index 8ceb03f323..6cae5fad06 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -2,40 +2,63 @@ - - - - - - - - + android:key="SETTINGS_RAGE_SHAKE_CATEGORY_KEY" + android:title="@string/settings_rageshake"> - + - + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index dd6a89ff1f..9a9185cdc7 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -81,13 +81,4 @@ - - - - - - - \ No newline at end of file From 96c9293edc69c62497d254bd1bfdc7b76b5df016 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 18:12:44 +0100 Subject: [PATCH 058/105] Rageshake: vibrate --- CHANGES.md | 1 + vector/src/main/AndroidManifest.xml | 1 + .../im/vector/riotx/core/hardware/vibrator.kt | 32 +++++++++++++++++++ .../features/navigation/DefaultNavigator.kt | 4 +-- .../riotx/features/navigation/Navigator.kt | 3 +- .../riotx/features/rageshake/RageShake.kt | 8 ++++- .../settings/VectorSettingsActivity.kt | 15 +++++++-- .../xml/vector_settings_developer_mode.xml | 4 +-- 8 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/hardware/vibrator.kt diff --git a/CHANGES.md b/CHANGES.md index 67b430e79b..adbba84260 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Improvements 🙌: - Fix autocompletion issues and add support for rooms and groups - Introduce developer mode in the settings (#796) - Improve devices list screen + - Add settings for rageshake sensibility Other changes: - diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 124763916b..308ca60094 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + = Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(durationMillis, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + @Suppress("DEPRECATION") + vibrator.vibrate(durationMillis) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 08ff11217d..48422056b4 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -120,8 +120,8 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } - override fun openSettings(context: Context) { - val intent = VectorSettingsActivity.getIntent(context) + override fun openSettings(context: Context, directAccess: Int) { + val intent = VectorSettingsActivity.getIntent(context, directAccess) context.startActivity(intent) } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 278c8fdba0..60045984c3 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.navigation import android.app.Activity import android.content.Context import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom +import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.SharedData interface Navigator { @@ -39,7 +40,7 @@ interface Navigator { fun openRoomsFiltering(context: Context) - fun openSettings(context: Context) + fun openSettings(context: Context, directAccess: Int = VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ROOT) fun openDebug(context: Context) diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt index bbd1090d98..6d5c780202 100644 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt @@ -23,11 +23,15 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.squareup.seismic.ShakeDetector import im.vector.riotx.R +import im.vector.riotx.core.hardware.vibrate +import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.settings.VectorPreferences +import im.vector.riotx.features.settings.VectorSettingsActivity import javax.inject.Inject class RageShake @Inject constructor(private val activity: AppCompatActivity, private val bugReporter: BugReporter, + private val navigator: Navigator, private val vectorPreferences: VectorPreferences) : ShakeDetector.Listener { private var shakeDetector: ShakeDetector? = null @@ -56,6 +60,7 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, override fun hearShake() { val i = interceptor if (i != null) { + vibrate(activity) i.invoke() } else { if (dialogDisplayed) { @@ -63,6 +68,7 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, return } + vibrate(activity) dialogDisplayed = true AlertDialog.Builder(activity) @@ -80,7 +86,7 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, } private fun openSettings() { - // TODO + navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_DEVELOPER) } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 16484224af..810547f610 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -54,7 +54,12 @@ class VectorSettingsActivity : VectorBaseActivity(), if (isFirstCreation()) { // display the fragment - replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) + when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { + EXTRA_DIRECT_ACCESS_DEVELOPER -> + replaceFragment(R.id.vector_settings_page, VectorSettingsDeveloperModeFragment::class.java, null, FRAGMENT_TAG) + else -> + replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) + } } supportFragmentManager.addOnBackStackChangedListener(this) @@ -111,7 +116,13 @@ class VectorSettingsActivity : VectorBaseActivity(), } companion object { - fun getIntent(context: Context) = Intent(context, VectorSettingsActivity::class.java) + fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java) + .apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) } + + private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS" + + const val EXTRA_DIRECT_ACCESS_ROOT = 0 + const val EXTRA_DIRECT_ACCESS_DEVELOPER = 1 private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" } diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml index 6cae5fad06..d3f960b31b 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -14,10 +14,10 @@ android:defaultValue="13" android:dependency="SETTINGS_USE_RAGE_SHAKE_KEY" android:key="SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY" - android:max="17" + android:max="15" android:summary="@string/settings_rageshake_detection_threshold_summary" android:title="@string/settings_rageshake_detection_threshold" - app:min="9" /> + app:min="11" /> From 8e478e78e13783482d1b75bd847830df0d788a96 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 18:17:54 +0100 Subject: [PATCH 059/105] Disable pref unused --- vector/src/main/res/xml/vector_settings_general.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index cd94da2d15..327815cb1d 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -49,7 +49,8 @@ + android:title="@string/settings_contact" + app:isPreferenceVisible="@bool/false_not_implemented"> Date: Thu, 2 Jan 2020 18:27:46 +0100 Subject: [PATCH 060/105] Remove Preference divider and cleanup prefs --- .../preference/VectorPreferenceDivider.kt | 36 ---------- .../features/settings/VectorPreferences.kt | 11 --- .../VectorSettingsSecurityPrivacyFragment.kt | 22 ------ .../res/layout/vector_preference_divider.xml | 21 ------ vector/src/main/res/values/attrs.xml | 3 - vector/src/main/res/values/theme_black.xml | 3 - vector/src/main/res/values/theme_dark.xml | 3 - vector/src/main/res/values/theme_light.xml | 3 - vector/src/main/res/values/theme_status.xml | 3 - .../main/res/xml/vector_settings_general.xml | 6 -- .../res/xml/vector_settings_help_about.xml | 68 +++++++++---------- ...ings_notification_advanced_preferences.xml | 2 - .../res/xml/vector_settings_notifications.xml | 6 +- .../res/xml/vector_settings_preferences.xml | 4 -- .../xml/vector_settings_security_privacy.xml | 8 --- 15 files changed, 32 insertions(+), 167 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt delete mode 100644 vector/src/main/res/layout/vector_preference_divider.xml diff --git a/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt b/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt deleted file mode 100644 index f9f9da644b..0000000000 --- a/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2018 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.core.preference - -import android.content.Context -import android.util.AttributeSet -import androidx.preference.Preference -import im.vector.riotx.R - -/** - * Divider for Preference screen - */ -class VectorPreferenceDivider @JvmOverloads constructor(context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, - defStyleRes: Int = 0 -) : Preference(context, attrs, defStyleAttr, defStyleRes) { - - init { - layoutResource = R.layout.vector_preference_divider - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index a86c3625ce..0ec67789fe 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -743,17 +743,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getInt(SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY, ShakeDetector.SENSITIVITY_MEDIUM) } - /** - * Update the rage shake status. - * - * @param isEnabled true to enable the rage shake - */ - fun setUseRageshake(isEnabled: Boolean) { - defaultPrefs.edit { - putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled) - } - } - /** * Tells if all the events must be displayed ie even the redacted events. * diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 72cc0884c9..cf5273d5a4 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -38,7 +38,6 @@ import im.vector.riotx.core.intent.analyseIntent import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.preference.VectorPreference -import im.vector.riotx.core.preference.VectorPreferenceDivider import im.vector.riotx.core.utils.* import im.vector.riotx.features.crypto.keys.KeysExporter import im.vector.riotx.features.crypto.keys.KeysImporter @@ -61,20 +60,11 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private val mCryptographyCategory by lazy { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!! } - private val mCryptographyCategoryDivider by lazy { - findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY)!! - } // cryptography manage private val mCryptographyManageCategory by lazy { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!! } - private val mCryptographyManageCategoryDivider by lazy { - findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY)!! - } // displayed pushers - private val mPushersSettingsDivider by lazy { - findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY)!! - } private val mPushersSettingsCategory by lazy { findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!! } @@ -131,16 +121,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( true } } - - // Rageshake Management - findPreference(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!!.let { - it.isChecked = vectorPreferences.useRageshake() - - it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - vectorPreferences.setUseRageshake(newValue as Boolean) - true - } - } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { @@ -333,11 +313,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private fun removeCryptographyPreference() { preferenceScreen.let { it.removePreference(mCryptographyCategory) - it.removePreference(mCryptographyCategoryDivider) // Also remove keys management section it.removePreference(mCryptographyManageCategory) - it.removePreference(mCryptographyManageCategoryDivider) } } diff --git a/vector/src/main/res/layout/vector_preference_divider.xml b/vector/src/main/res/layout/vector_preference_divider.xml deleted file mode 100644 index 81f7a091e5..0000000000 --- a/vector/src/main/res/layout/vector_preference_divider.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index c30a1d99d9..aca2a7fa5f 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -63,9 +63,6 @@ - - - diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index 7398a4bcb7..7bce009429 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -72,9 +72,6 @@ @color/primary_color_dark_black @color/list_divider_color_black - - @color/list_divider_color_black - #FF4D4D4D @drawable/pill_receipt_black diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index f61a89482a..a05081eec7 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -139,9 +139,6 @@ #FF61708b - - @color/list_divider_color_dark - @android:color/white @color/riotx_accent diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index aa343a11fc..9cea0e52b7 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -139,9 +139,6 @@ #FF61708b - - @color/list_divider_color_light - @android:color/black @color/riotx_accent diff --git a/vector/src/main/res/values/theme_status.xml b/vector/src/main/res/values/theme_status.xml index 322522c723..421632e64c 100644 --- a/vector/src/main/res/values/theme_status.xml +++ b/vector/src/main/res/values/theme_status.xml @@ -88,9 +88,6 @@ #a0a29f - - #e1e1e1 - @color/accent_color_status @color/riotx_accent diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index 327815cb1d..c49eca825a 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -45,8 +45,6 @@ - - - - - - - + - + - + - + - + - + - + - + - - - - - + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml b/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml index 32b6a2b499..b5f01d98f6 100644 --- a/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml @@ -36,8 +36,6 @@ - - - - - - + android:title="@string/settings_notifications_targets" /--> \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 7d490d124b..7698372053 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -99,8 +99,6 @@ - - - - diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index 9a9185cdc7..234ecbe647 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -27,8 +27,6 @@ - - - - @@ -64,10 +60,6 @@ - - Date: Thu, 2 Jan 2020 18:45:44 +0100 Subject: [PATCH 061/105] Auto-review --- .../im/vector/riotx/core/di/FragmentModule.kt | 2 +- .../settings/devices/DevicesController.kt | 3 --- .../features/settings/devices/DevicesViewModel.kt | 4 ++-- .../devices/VectorSettingsDevicesFragment.kt | 15 ++++----------- .../res/xml/vector_settings_developer_mode.xml | 2 +- 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 88af219c78..5565369006 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -227,7 +227,7 @@ interface FragmentModule { @Binds @IntoMap @FragmentKey(VectorSettingsIgnoredUsersFragment::class) - fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment + fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt index db61db429b..e0a379be83 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -61,9 +61,6 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor listener { callback?.retry() } } is Success -> - // Build the devices portion of the settings. - // Each row correspond to a device ID and its corresponding device name. Clicking on the row - // display a dialog containing: the device ID, the device name and the "last seen" information. devices() // sort before display: most recent first .sortByLastSeen() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt index 1fc56e76fd..b2b015a3f0 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -80,8 +80,8 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic /** * Force the refresh of the devices list. - * The devices list is the list of the devices where the user as logged in. - * It can be any mobile device, as any browser. + * The devices list is the list of the devices where the user is logged in. + * It can be any mobile devices, and any browsers. */ private fun refreshDevicesList() { if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt index 4168e3eeb7..4d14930fce 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -91,13 +91,6 @@ class VectorSettingsDevicesFragment @Inject constructor( .show() } - /** - * Display a dialog containing the device ID, the device name and the "last seen" information. - * This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog]) - * - * @param deviceInfo the device information - * @param isCurrentDevice true if this is the current device - */ override fun onDeviceClicked(deviceInfo: DeviceInfo) { devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo)) } @@ -117,14 +110,14 @@ class VectorSettingsDevicesFragment @Inject constructor( /** * Display an alert dialog to rename a device * - * @param aDeviceInfoToRename device info + * @param deviceInfo device info */ - private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) { + private fun displayDeviceRenameDialog(deviceInfo: DeviceInfo) { val inflater = requireActivity().layoutInflater val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) val input = layout.findViewById(R.id.edit_text) - input.setText(aDeviceInfoToRename.displayName) + input.setText(deviceInfo.displayName) AlertDialog.Builder(requireActivity()) .setTitle(R.string.devices_details_device_name) @@ -132,7 +125,7 @@ class VectorSettingsDevicesFragment @Inject constructor( .setPositiveButton(R.string.ok) { _, _ -> val newName = input.text.toString() - devicesViewModel.handle(DevicesAction.Rename(aDeviceInfoToRename, newName)) + devicesViewModel.handle(DevicesAction.Rename(deviceInfo, newName)) } .setNegativeButton(R.string.cancel, null) .show() diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml index d3f960b31b..b7b800052f 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -10,7 +10,7 @@ android:key="SETTINGS_USE_RAGE_SHAKE_KEY" android:title="@string/send_bug_report_rage_shake" /> - Date: Thu, 2 Jan 2020 18:53:35 +0100 Subject: [PATCH 062/105] Fix crash when opening room creation screen from the room filtering screen --- CHANGES.md | 1 + .../features/roomdirectory/createroom/CreateRoomActivity.kt | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index e2cd2915b5..4e39b72de7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ Other changes: - Bugfix 🐛: + - Fix crash when opening room creation screen from the room filtering screen - Fix avatar image disappearing (#777) - Fix read marker banner when permalink diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt index a83208c98a..2831457224 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt @@ -22,6 +22,7 @@ import android.os.Bundle import androidx.appcompat.widget.Toolbar import com.airbnb.mvrx.viewModel import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity @@ -45,6 +46,10 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable { configureToolbar(toolbar) } + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + override fun initUiAndData() { if (isFirstCreation()) { addFragment(R.id.simpleFragmentContainer, CreateRoomFragment::class.java) From c2e7e330505a09734f9369358e93067af2acdc64 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Jan 2020 16:20:00 +0100 Subject: [PATCH 063/105] Update SessionParamsEntity primaryKey to include deviceId --- CHANGES.md | 2 +- .../matrix/android/internal/auth/SessionId.kt | 23 ++++++++++++++++ .../internal/auth/db/AuthRealmMigration.kt | 26 ++++++++++++++++++- .../internal/auth/db/SessionParamsEntity.kt | 3 ++- .../internal/auth/db/SessionParamsMapper.kt | 2 ++ 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt diff --git a/CHANGES.md b/CHANGES.md index 4e39b72de7..638ba9bf49 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,7 @@ Improvements 🙌: - Fix autocompletion issues and add support for rooms and groups Other changes: - - + - Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800) Bugfix 🐛: - Fix crash when opening room creation screen from the room filtering screen diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt new file mode 100644 index 0000000000..2f39806aa5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth + +import im.vector.matrix.android.internal.util.md5 + +internal fun createSessionId(userId: String, deviceId: String?): String { + return (if (deviceId.isNullOrBlank()) userId else "$userId|$deviceId").md5() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt index 83bf7b7822..36f3bce6b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt @@ -16,6 +16,9 @@ package im.vector.matrix.android.internal.auth.db +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.auth.createSessionId +import im.vector.matrix.android.internal.di.MoshiProvider import io.realm.DynamicRealm import io.realm.RealmMigration import timber.log.Timber @@ -23,7 +26,7 @@ import timber.log.Timber internal object AuthRealmMigration : RealmMigration { // Current schema version - const val SCHEMA_VERSION = 2L + const val SCHEMA_VERSION = 3L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") @@ -53,5 +56,26 @@ internal object AuthRealmMigration : RealmMigration { ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } } + + if (oldVersion <= 2) { + Timber.d("Step 2 -> 3") + Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId") + + realm.schema.get("SessionParamsEntity") + ?.removePrimaryKey() + ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java) + ?.setRequired(SessionParamsEntityFields.SESSION_ID, true) + ?.transform { + val userId = it.getString(SessionParamsEntityFields.USER_ID) + val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON) + + val credentials = MoshiProvider.providesMoshi() + .adapter(Credentials::class.java) + .fromJson(credentialsJson) + + it.set(SessionParamsEntityFields.SESSION_ID, createSessionId(userId, credentials?.deviceId)) + } + ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt index 92511dccf7..72eed95fcc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt @@ -20,7 +20,8 @@ import io.realm.RealmObject import io.realm.annotations.PrimaryKey internal open class SessionParamsEntity( - @PrimaryKey var userId: String = "", + @PrimaryKey var sessionId: String = "", + var userId: String = "", var credentialsJson: String = "", var homeServerConnectionConfigJson: String = "", // Set to false when the token is invalid and the user has been soft logged out diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt index 72e8087f3f..d4ba1eb818 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.Moshi import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.internal.auth.createSessionId import javax.inject.Inject internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { @@ -49,6 +50,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { return null } return SessionParamsEntity( + createSessionId(sessionParams.credentials.userId, sessionParams.credentials.deviceId), sessionParams.credentials.userId, credentialsJson, homeServerConnectionConfigJson, From 160927e7b53323d8bf74d379dacd191cfa7c7484 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Jan 2020 14:25:19 +0100 Subject: [PATCH 064/105] Split code into several methods --- .../internal/auth/db/AuthRealmMigration.kt | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt index 36f3bce6b6..7d7f8cc22c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt @@ -31,51 +31,55 @@ internal object AuthRealmMigration : RealmMigration { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") - if (oldVersion <= 0) { - Timber.d("Step 0 -> 1") - Timber.d("Create PendingSessionEntity") + if (oldVersion <= 0) migrateTo1(realm) + if (oldVersion <= 1) migrateTo2(realm) + if (oldVersion <= 2) migrateTo3(realm) + } - realm.schema.create("PendingSessionEntity") - .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) - .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) - .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java) - .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true) - .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java) - .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true) - .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java) - .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java) - .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) - .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) - } + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Create PendingSessionEntity") - if (oldVersion <= 1) { - Timber.d("Step 1 -> 2") - Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") + realm.schema.create("PendingSessionEntity") + .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) + .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) + .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java) + .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true) + .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java) + .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true) + .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java) + .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java) + .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) + .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) + } - realm.schema.get("SessionParamsEntity") - ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) - ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } - } + private fun migrateTo2(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") - if (oldVersion <= 2) { - Timber.d("Step 2 -> 3") - Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId") + realm.schema.get("SessionParamsEntity") + ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) + ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } + } - realm.schema.get("SessionParamsEntity") - ?.removePrimaryKey() - ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java) - ?.setRequired(SessionParamsEntityFields.SESSION_ID, true) - ?.transform { - val userId = it.getString(SessionParamsEntityFields.USER_ID) - val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON) + private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") + Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId") - val credentials = MoshiProvider.providesMoshi() - .adapter(Credentials::class.java) - .fromJson(credentialsJson) + realm.schema.get("SessionParamsEntity") + ?.removePrimaryKey() + ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java) + ?.setRequired(SessionParamsEntityFields.SESSION_ID, true) + ?.transform { + val userId = it.getString(SessionParamsEntityFields.USER_ID) + val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON) - it.set(SessionParamsEntityFields.SESSION_ID, createSessionId(userId, credentials?.deviceId)) - } - ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID) - } + val credentials = MoshiProvider.providesMoshi() + .adapter(Credentials::class.java) + .fromJson(credentialsJson) + + it.set(SessionParamsEntityFields.SESSION_ID, createSessionId(userId, credentials?.deviceId)) + } + ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID) } } From 215abea10a7af80dfac28ba5299272c0531903e1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Jan 2020 14:49:26 +0100 Subject: [PATCH 065/105] Introduce @SessionId --- .../SessionRealmConfigurationFactory.kt | 8 +++++--- .../android/internal/di/StringQualifiers.kt | 8 ++++++++ .../internal/session/DefaultFileService.kt | 8 ++++---- .../android/internal/session/SessionModule.kt | 20 +++++++++++++++++-- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt index bc806a56a4..0d0c351a2b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database import android.content.Context import im.vector.matrix.android.internal.database.model.SessionRealmModule +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.UserCacheDirectory import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.session.SessionModule @@ -37,13 +38,14 @@ private const val REALM_NAME = "disk_store.realm" */ internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils, @UserCacheDirectory val directory: File, + @SessionId val sessionId: String, @UserMd5 val userMd5: String, context: Context) { private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) fun create(): RealmConfiguration { - val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) + val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) if (shouldClearRealm) { Timber.v("************************************************************") Timber.v("The realm file session was corrupted and couldn't be loaded.") @@ -53,7 +55,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val } sharedPreferences .edit() - .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", true) + .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true) .apply() val realmConfiguration = RealmConfiguration.Builder() @@ -71,7 +73,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val Timber.v("Successfully create realm instance") sharedPreferences .edit() - .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) + .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) .apply() } return realmConfiguration diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt index 0e38618590..7d2610903a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -31,3 +31,11 @@ internal annotation class UserId @Qualifier @Retention(AnnotationRetention.RUNTIME) internal annotation class UserMd5 + + +/** + * Used to inject the sessionId, which is defined as md5(userId|deviceId) + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index c160ac9b31..66b94cf68d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments -import im.vector.matrix.android.internal.di.UserMd5 +import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.md5 @@ -42,7 +42,7 @@ import java.io.IOException import javax.inject.Inject internal class DefaultFileService @Inject constructor(private val context: Context, - @UserMd5 private val userMd5: String, + @SessionId private val sessionId: String, private val contentUrlResolver: ContentUrlResolver, private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { @@ -103,9 +103,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte return when (downloadMode) { FileService.DownloadMode.FOR_INTERNAL_USE -> { // Create dir tree (MF stands for Matrix File): - // /MF/// + // /MF/// val tmpFolderRoot = File(context.cacheDir, "MF") - val tmpFolderUser = File(tmpFolderRoot, userMd5) + val tmpFolderUser = File(tmpFolderRoot, sessionId) File(tmpFolderUser, id.md5()) } FileService.DownloadMode.TO_EXPORT -> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 0e88894969..4c135b960c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService +import im.vector.matrix.android.internal.auth.createSessionId import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.* @@ -83,11 +84,26 @@ internal abstract class SessionModule { return userId.md5() } + @JvmStatic + @SessionId + @Provides + fun providesSessionId(credentials: Credentials): String { + return createSessionId(credentials.userId, credentials.deviceId) + } + @JvmStatic @Provides @UserCacheDirectory - fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File { - return File(context.filesDir, userMd5) + fun providesFilesDir(@UserMd5 userMd5: String, + @SessionId sessionId: String, + context: Context): File { + // Temporary code for migration + val old = File(context.filesDir, userMd5) + if (old.exists()) { + old.renameTo(File(context.filesDir, sessionId)) + } + + return File(context.filesDir, sessionId) } @JvmStatic From f432d15757b01707285fe6498a444aba228271cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Jan 2020 14:55:06 +0100 Subject: [PATCH 066/105] Ensure key aliases are always computed the same way --- .../im/vector/matrix/android/internal/crypto/CryptoModule.kt | 4 ++-- .../internal/database/SessionRealmConfigurationFactory.kt | 2 +- .../vector/matrix/android/internal/session/SessionModule.kt | 3 +-- .../matrix/android/internal/session/signout/SignOutTask.kt | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index a12f6e40ce..443f748528 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -47,7 +47,7 @@ internal abstract class CryptoModule { @Module companion object { - internal const val DB_ALIAS_PREFIX = "crypto_module_" + internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5" @JvmStatic @Provides @@ -59,7 +59,7 @@ internal abstract class CryptoModule { return RealmConfiguration.Builder() .directory(directory) .apply { - realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5") + realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5)) } .name("crypto_store.realm") .modules(RealmCryptoStoreModule()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt index 0d0c351a2b..b9d95035d2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt @@ -62,7 +62,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val .directory(directory) .name(REALM_NAME) .apply { - realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5") + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) } .modules(SessionRealmModule()) .deleteRealmIfMigrationNeeded() 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 4c135b960c..437a559ea1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -55,8 +55,7 @@ internal abstract class SessionModule { @Module companion object { - - internal const val DB_ALIAS_PREFIX = "session_db_" + internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5" @JvmStatic @Provides diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index 51cb22c988..b43bfa603c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -97,8 +97,8 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte userFile.deleteRecursively() Timber.d("SignOut: clear the database keys") - realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5) - realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5) + realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5)) + realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5)) // Sanity check if (BuildConfig.DEBUG) { From ce73007157c71e1dbfaf367c6d874392d48cb598 Mon Sep 17 00:00:00 2001 From: Marcus Hoffmann Date: Mon, 6 Jan 2020 00:51:41 +0100 Subject: [PATCH 067/105] call /join/{roomIdOrAlias} instead of /rooms/{roomId}/join The former endpoint doesn't work for joining over federation, the server_name parameter is ignored. Fixes #697 Signed-off-by: Marcus Hoffmann --- CHANGES.md | 1 + .../vector/matrix/android/internal/session/room/RoomAPI.kt | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4e39b72de7..02a05ce905 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ Bugfix 🐛: - Fix crash when opening room creation screen from the room filtering screen - Fix avatar image disappearing (#777) - Fix read marker banner when permalink + - Fix joining upgraded rooms (#697) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index c5b3f03d35..6896788de9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -212,11 +212,12 @@ internal interface RoomAPI { /** * Join the given room. * - * @param roomId the room id + * @param roomIdOrAlias the room id or alias + * @param server_name the servers to attempt to join the room through * @param params the request body */ - @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") - fun join(@Path("roomId") roomId: String, + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}") + fun join(@Path("roomIdOrAlias") roomIdOrAlias: String, @Query("server_name") viaServers: List, @Body params: Map): Call From 9e8217082c445b2a92ffbb60e149dc5263ec4a85 Mon Sep 17 00:00:00 2001 From: Marcus Hoffmann Date: Mon, 6 Jan 2020 03:45:22 +0100 Subject: [PATCH 068/105] set homeserver field when populating room directory list fixes #807 Signed-off-by: Marcus Hoffmann --- CHANGES.md | 1 + .../features/roomdirectory/picker/RoomDirectoryListCreator.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 4e39b72de7..fc916060f1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ Bugfix 🐛: - Fix crash when opening room creation screen from the room filtering screen - Fix avatar image disappearing (#777) - Fix read marker banner when permalink + - Fix matrix.org room directory not being browsable Translations 🗣: - diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt index 17693d9ad6..4073929a4f 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryListCreator.kt @@ -48,6 +48,7 @@ class RoomDirectoryListCreator @Inject constructor(private val stringArrayProvid if (it != userHsName) { // Use the server name as a default display name result.add(RoomDirectoryData( + homeServer = it, displayName = it, includeAllNetworks = true )) From a00f51a26447aae53a330d80b9f17799997a31f8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Jan 2020 10:32:36 +0100 Subject: [PATCH 069/105] Settings: rename "developer mode" to "advanced settings" --- .../riotx/features/rageshake/RageShake.kt | 2 +- .../settings/VectorSettingsActivity.kt | 8 +-- ...VectorSettingsAdvancedSettingsFragment.kt} | 6 +- vector/src/main/res/values/strings_riotX.xml | 1 + ... => vector_settings_advanced_settings.xml} | 60 +++++++++---------- .../src/main/res/xml/vector_settings_root.xml | 4 +- 6 files changed, 40 insertions(+), 41 deletions(-) rename vector/src/main/java/im/vector/riotx/features/settings/{VectorSettingsDeveloperModeFragment.kt => VectorSettingsAdvancedSettingsFragment.kt} (92%) rename vector/src/main/res/xml/{vector_settings_developer_mode.xml => vector_settings_advanced_settings.xml} (73%) diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt index 6d5c780202..effed19c59 100644 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt @@ -86,7 +86,7 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, } private fun openSettings() { - navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_DEVELOPER) + navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS) } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 810547f610..490805ea3c 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -55,9 +55,9 @@ class VectorSettingsActivity : VectorBaseActivity(), if (isFirstCreation()) { // display the fragment when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { - EXTRA_DIRECT_ACCESS_DEVELOPER -> - replaceFragment(R.id.vector_settings_page, VectorSettingsDeveloperModeFragment::class.java, null, FRAGMENT_TAG) - else -> + EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> + replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) + else -> replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) } } @@ -122,7 +122,7 @@ class VectorSettingsActivity : VectorBaseActivity(), private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS" const val EXTRA_DIRECT_ACCESS_ROOT = 0 - const val EXTRA_DIRECT_ACCESS_DEVELOPER = 1 + const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1 private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt similarity index 92% rename from vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt rename to vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt index f700759fd0..43adcf6335 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt @@ -23,10 +23,10 @@ import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.preference.VectorSwitchPreference import im.vector.riotx.features.rageshake.RageShake -class VectorSettingsDeveloperModeFragment : VectorSettingsBaseFragment() { +class VectorSettingsAdvancedSettingsFragment : VectorSettingsBaseFragment() { - override var titleRes = R.string.settings_developer_mode - override val preferenceXmlRes = R.xml.vector_settings_developer_mode + override var titleRes = R.string.settings_advanced_settings + override val preferenceXmlRes = R.xml.vector_settings_advanced_settings private var rageshake: RageShake? = null diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 1134b4be0f..5b2b2e99e8 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -7,6 +7,7 @@ See all my devices + Advanced settings Developer mode The developer mode activates hidden features and may also make the application less stable. For developers only! Rageshake diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml similarity index 73% rename from vector/src/main/res/xml/vector_settings_developer_mode.xml rename to vector/src/main/res/xml/vector_settings_advanced_settings.xml index b7b800052f..93ff104a81 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -2,25 +2,6 @@ - - - - - - - - - + - + - + - + + + + + + + + + diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index 0a59570050..f2e13d8111 100644 --- a/vector/src/main/res/xml/vector_settings_root.xml +++ b/vector/src/main/res/xml/vector_settings_root.xml @@ -47,8 +47,8 @@ + android:title="@string/settings_advanced_settings" + app:fragment="im.vector.riotx.features.settings.VectorSettingsAdvancedSettingsFragment" /> Date: Mon, 6 Jan 2020 10:51:30 +0100 Subject: [PATCH 070/105] Improve (a bit) the devices list UX/UI --- .../settings/devices/DevicesController.kt | 71 +++++++++++++++---- .../devices/VectorSettingsDevicesFragment.kt | 2 +- vector/src/main/res/values/strings_riotX.xml | 2 + 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt index e0a379be83..18c0965f86 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -23,12 +23,16 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.extensions.sortByLastSeen import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.riotx.R import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.ui.list.genericItemHeader import javax.inject.Inject -class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter) : EpoxyController() { +class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter, + private val stringProvider: StringProvider) : EpoxyController() { var callback: Callback? = null private var viewState: DevicesViewState? = null @@ -61,21 +65,58 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor listener { callback?.retry() } } is Success -> - devices() - // sort before display: most recent first - .sortByLastSeen() - .forEachIndexed { idx, deviceInfo -> - val isCurrentDevice = deviceInfo.deviceId == state.myDeviceId - deviceItem { - id("device$idx") - deviceInfo(deviceInfo) - currentDevice(isCurrentDevice) - buttonsVisible(deviceInfo.deviceId == state.currentExpandedDeviceId) - itemClickAction { callback?.onDeviceClicked(deviceInfo) } - renameClickAction { callback?.onRenameDevice(deviceInfo) } - deleteClickAction { callback?.onDeleteDevice(deviceInfo) } - } + buildDevicesList(devices(), state.myDeviceId, state.currentExpandedDeviceId) + } + } + + private fun buildDevicesList(devices: List, myDeviceId: String, currentExpandedDeviceId: String?) { + // Current device + genericItemHeader { + id("current") + text(stringProvider.getString(R.string.devices_current_device)) + } + + devices + .filter { + it.deviceId == myDeviceId + } + .forEachIndexed { idx, deviceInfo -> + deviceItem { + id("myDevice$idx") + deviceInfo(deviceInfo) + currentDevice(true) + buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId) + itemClickAction { callback?.onDeviceClicked(deviceInfo) } + renameClickAction { callback?.onRenameDevice(deviceInfo) } + deleteClickAction { callback?.onDeleteDevice(deviceInfo) } + } + } + + // Other devices + if (devices.size > 1) { + genericItemHeader { + id("others") + text(stringProvider.getString(R.string.devices_other_devices)) + } + + devices + .filter { + it.deviceId != myDeviceId + } + // sort before display: most recent first + .sortByLastSeen() + .forEachIndexed { idx, deviceInfo -> + val isCurrentDevice = deviceInfo.deviceId == myDeviceId + deviceItem { + id("device$idx") + deviceInfo(deviceInfo) + currentDevice(isCurrentDevice) + buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId) + itemClickAction { callback?.onDeviceClicked(deviceInfo) } + renameClickAction { callback?.onRenameDevice(deviceInfo) } + deleteClickAction { callback?.onDeleteDevice(deviceInfo) } } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt index 4d14930fce..465b3ba0fb 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -60,7 +60,7 @@ class VectorSettingsDevicesFragment @Inject constructor( waiting_view_status_text.setText(R.string.please_wait) waiting_view_status_text.isVisible = true devicesController.callback = this - recyclerView.configureWith(devicesController) + recyclerView.configureWith(devicesController, showDivider = true) devicesViewModel.requestErrorLiveData.observeEvent(this) { displayErrorDialog(it) // Password is maybe not good, for safety measure, reset it here diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 5b2b2e99e8..1502740112 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -15,5 +15,7 @@ Shake your phone to test the detection threshold Shake detected! Settings + Current device + Other devices From d73a1135ae8afbe1246a52bd6bd991698f83f35e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:01:07 +0100 Subject: [PATCH 071/105] Extract AutoComplete feature from RoomDetailFragment --- .../home/room/detail/AutoCompleter.kt | 241 ++++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 192 +------------- 2 files changed, 247 insertions(+), 186 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt new file mode 100644 index 0000000000..5e8a87ba51 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -0,0 +1,241 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail + +import android.graphics.drawable.ColorDrawable +import android.text.Editable +import android.text.Spannable +import android.widget.EditText +import androidx.fragment.app.Fragment +import com.otaliastudios.autocomplete.Autocomplete +import com.otaliastudios.autocomplete.AutocompleteCallback +import com.otaliastudios.autocomplete.CharPolicy +import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.api.util.toRoomAliasMatrixItem +import im.vector.riotx.R +import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter +import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy +import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter +import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter +import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter +import im.vector.riotx.features.command.Command +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState +import im.vector.riotx.features.html.PillImageSpan +import im.vector.riotx.features.themes.ThemeUtils +import javax.inject.Inject + +class AutoCompleter @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val commandAutocompletePolicy: CommandAutocompletePolicy, + private val autocompleteCommandPresenter: AutocompleteCommandPresenter, + private val autocompleteUserPresenter: AutocompleteUserPresenter, + private val autocompleteRoomPresenter: AutocompleteRoomPresenter, + private val autocompleteGroupPresenter: AutocompleteGroupPresenter +) { + private lateinit var fragment: Fragment + + fun enterSpecialMode() { + commandAutocompletePolicy.enabled = false + } + + fun exitSpecialMode() { + commandAutocompletePolicy.enabled = true + } + + private val glideRequests by lazy { + GlideApp.with(fragment) + } + + fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) { + this.fragment = fragment + + val elevation = 6f + val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background)) + Autocomplete.on(editText) + .with(commandAutocompletePolicy) + .with(autocompleteCommandPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { + editable.clear() + editable + .append(item.command) + .append(" ") + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + + autocompleteRoomPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('#', true)) + .with(autocompleteRoomPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { + // Detect last '#' and remove it + var startIndex = editable.lastIndexOf("#") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val matrixItem = item.toRoomAliasMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + + autocompleteGroupPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('+', true)) + .with(autocompleteGroupPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { + // Detect last '+' and remove it + var startIndex = editable.lastIndexOf("+") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val matrixItem = item.toMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + + autocompleteUserPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('@', true)) + .with(autocompleteUserPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: User): Boolean { + // Detect last '@' and remove it + var startIndex = editable.lastIndexOf("@") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val matrixItem = item.toMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + } + + fun render(state: TextComposerViewState) { + autocompleteUserPresenter.render(state.asyncUsers) + autocompleteRoomPresenter.render(state.asyncRooms) + autocompleteGroupPresenter.render(state.asyncGroups) + } + + interface AutoCompleterListener : + AutocompleteUserPresenter.Callback, + AutocompleteRoomPresenter.Callback, + AutocompleteGroupPresenter.Callback +} 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 334445870c..4ae79e8215 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -20,12 +20,10 @@ import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.DialogInterface import android.content.Intent -import android.graphics.drawable.ColorDrawable import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Parcelable -import android.text.Editable import android.text.Spannable import android.view.* import android.widget.TextView @@ -52,25 +50,18 @@ import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText -import com.otaliastudios.autocomplete.Autocomplete -import com.otaliastudios.autocomplete.AutocompleteCallback -import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent -import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem -import im.vector.matrix.android.api.util.toRoomAliasMatrixItem import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer @@ -84,11 +75,6 @@ import im.vector.riotx.core.utils.* import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.ContactAttachment -import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter -import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy -import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter -import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter -import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.getColorFromUserId @@ -117,7 +103,6 @@ import im.vector.riotx.features.permalink.PermalinkHandler import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.share.SharedData -import im.vector.riotx.features.themes.ThemeUtils import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import kotlinx.android.parcel.Parcelize @@ -142,11 +127,7 @@ class RoomDetailFragment @Inject constructor( private val session: Session, private val avatarRenderer: AvatarRenderer, private val timelineEventController: TimelineEventController, - private val commandAutocompletePolicy: CommandAutocompletePolicy, - private val autocompleteCommandPresenter: AutocompleteCommandPresenter, - private val autocompleteUserPresenter: AutocompleteUserPresenter, - private val autocompleteRoomPresenter: AutocompleteRoomPresenter, - private val autocompleteGroupPresenter: AutocompleteGroupPresenter, + private val autoCompleter: AutoCompleter, private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, val roomDetailViewModelFactory: RoomDetailViewModel.Factory, @@ -156,9 +137,7 @@ class RoomDetailFragment @Inject constructor( ) : VectorBaseFragment(), TimelineEventController.Callback, - AutocompleteUserPresenter.Callback, - AutocompleteRoomPresenter.Callback, - AutocompleteGroupPresenter.Callback, + AutoCompleter.AutoCompleterListener, VectorInviteView.Callback, JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, @@ -397,7 +376,7 @@ class RoomDetailFragment @Inject constructor( } private fun renderRegularMode(text: String) { - commandAutocompletePolicy.enabled = true + autoCompleter.exitSpecialMode() composerLayout.collapse() updateComposerText(text) @@ -408,7 +387,7 @@ class RoomDetailFragment @Inject constructor( @DrawableRes iconRes: Int, @StringRes descriptionRes: Int, defaultContent: String) { - commandAutocompletePolicy.enabled = false + autoCompleter.enterSpecialMode() // switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { text = event.getDisambiguatedDisplayName() @@ -580,164 +559,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupComposer() { - val elevation = 6f - val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) - Autocomplete.on(composerLayout.composerEditText) - .with(commandAutocompletePolicy) - .with(autocompleteCommandPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { - editable.clear() - editable - .append(item.command) - .append(" ") - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() - - autocompleteRoomPresenter.callback = this - Autocomplete.on(composerLayout.composerEditText) - .with(CharPolicy('#', true)) - .with(autocompleteRoomPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { - // Detect last '#' and remove it - var startIndex = editable.lastIndexOf("#") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toRoomAliasMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - requireContext(), - matrixItem - ) - span.bind(composerLayout.composerEditText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() - - autocompleteGroupPresenter.callback = this - Autocomplete.on(composerLayout.composerEditText) - .with(CharPolicy('+', true)) - .with(autocompleteGroupPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { - // Detect last '+' and remove it - var startIndex = editable.lastIndexOf("+") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - requireContext(), - matrixItem - ) - span.bind(composerLayout.composerEditText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() - - autocompleteUserPresenter.callback = this - Autocomplete.on(composerLayout.composerEditText) - .with(CharPolicy('@', true)) - .with(autocompleteUserPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: User): Boolean { - // Detect last '@' and remove it - var startIndex = editable.lastIndexOf("@") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - requireContext(), - matrixItem - ) - span.bind(composerLayout.composerEditText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() + autoCompleter.setup(this, composerLayout.composerEditText, this) composerLayout.callback = object : TextComposerView.Callback { override fun onAddAttachment() { @@ -834,9 +656,7 @@ class RoomDetailFragment @Inject constructor( } private fun renderTextComposerState(state: TextComposerViewState) { - autocompleteUserPresenter.render(state.asyncUsers) - autocompleteRoomPresenter.render(state.asyncRooms) - autocompleteGroupPresenter.render(state.asyncGroups) + autoCompleter.render(state) } private fun renderTombstoneEventHandling(async: Async) { From c4fe0bdb7f74695c501d7895c3db199ea892fac5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:08:28 +0100 Subject: [PATCH 072/105] Split into small methods --- .../home/room/detail/AutoCompleter.kt | 118 ++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 5e8a87ba51..7edba64a05 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable import android.widget.EditText @@ -68,12 +69,19 @@ class AutoCompleter @Inject constructor( fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) { this.fragment = fragment - val elevation = 6f val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background)) + + setupCommands(backgroundDrawable, editText) + setupUsers(backgroundDrawable, editText, listener) + setupRooms(backgroundDrawable, editText, listener) + setupGroups(backgroundDrawable, editText, listener) + } + + private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) .with(commandAutocompletePolicy) .with(autocompleteCommandPresenter) - .with(elevation) + .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { @@ -88,12 +96,62 @@ class AutoCompleter @Inject constructor( } }) .build() + } + private fun setupUsers(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteUserPresenter.Callback) { + autocompleteUserPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('@', true)) + .with(autocompleteUserPresenter) + .with(ELEVATION) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: User): Boolean { + // Detect last '@' and remove it + var startIndex = editable.lastIndexOf("@") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val matrixItem = item.toMatrixItem() + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + } + + private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteRoomPresenter.Callback) { autocompleteRoomPresenter.callback = listener Autocomplete.on(editText) .with(CharPolicy('#', true)) .with(autocompleteRoomPresenter) - .with(elevation) + .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { @@ -134,12 +192,14 @@ class AutoCompleter @Inject constructor( } }) .build() + } + private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteGroupPresenter.Callback) { autocompleteGroupPresenter.callback = listener Autocomplete.on(editText) .with(CharPolicy('+', true)) .with(autocompleteGroupPresenter) - .with(elevation) + .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { @@ -180,52 +240,6 @@ class AutoCompleter @Inject constructor( } }) .build() - - autocompleteUserPresenter.callback = listener - Autocomplete.on(editText) - .with(CharPolicy('@', true)) - .with(autocompleteUserPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: User): Boolean { - // Detect last '@' and remove it - var startIndex = editable.lastIndexOf("@") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() } fun render(state: TextComposerViewState) { @@ -238,4 +252,8 @@ class AutoCompleter @Inject constructor( AutocompleteUserPresenter.Callback, AutocompleteRoomPresenter.Callback, AutocompleteGroupPresenter.Callback + + companion object { + private const val ELEVATION = 6f + } } From d88e5d8af8b7afea2c5dfc6c33068d5355d3fabb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:14:26 +0100 Subject: [PATCH 073/105] DRY --- .../home/room/detail/AutoCompleter.kt | 133 +++++------------- 1 file changed, 39 insertions(+), 94 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 7edba64a05..ef7715f91a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -28,6 +28,7 @@ import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toRoomAliasMatrixItem import im.vector.riotx.R @@ -77,6 +78,12 @@ class AutoCompleter @Inject constructor( setupGroups(backgroundDrawable, editText, listener) } + fun render(state: TextComposerViewState) { + autocompleteUserPresenter.render(state.asyncUsers) + autocompleteRoomPresenter.render(state.asyncRooms) + autocompleteGroupPresenter.render(state.asyncGroups) + } + private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) .with(commandAutocompletePolicy) @@ -107,36 +114,7 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: User): Boolean { - // Detect last '@' and remove it - var startIndex = editable.lastIndexOf("@") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - + insertMatrixItem(editText, editable, "@", item.toMatrixItem()) return true } @@ -155,36 +133,7 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { - // Detect last '#' and remove it - var startIndex = editable.lastIndexOf("#") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toRoomAliasMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - + insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem()) return true } @@ -203,36 +152,7 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { - // Detect last '+' and remove it - var startIndex = editable.lastIndexOf("+") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val matrixItem = item.toMatrixItem() - val displayName = matrixItem.getBestName() - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val span = PillImageSpan( - glideRequests, - avatarRenderer, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - + insertMatrixItem(editText, editable, "+", item.toMatrixItem()) return true } @@ -242,10 +162,35 @@ class AutoCompleter @Inject constructor( .build() } - fun render(state: TextComposerViewState) { - autocompleteUserPresenter.render(state.asyncUsers) - autocompleteRoomPresenter.render(state.asyncRooms) - autocompleteGroupPresenter.render(state.asyncGroups) + private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) { + // Detect last firstChar and remove it + var startIndex = editable.lastIndexOf(firstChar) + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val displayName = matrixItem.getBestName() + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val span = PillImageSpan( + glideRequests, + avatarRenderer, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } interface AutoCompleterListener : From 8597c2b9a2d99d5edcbf3fa2dd9ef89ba30f955e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:23:30 +0100 Subject: [PATCH 074/105] Improve API --- .../riotx/features/home/room/detail/AutoCompleter.kt | 12 ++++++------ .../features/home/room/detail/RoomDetailFragment.kt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index ef7715f91a..ba643e2d7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -53,7 +53,7 @@ class AutoCompleter @Inject constructor( private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val autocompleteGroupPresenter: AutocompleteGroupPresenter ) { - private lateinit var fragment: Fragment + private lateinit var editText: EditText fun enterSpecialMode() { commandAutocompletePolicy.enabled = false @@ -64,13 +64,13 @@ class AutoCompleter @Inject constructor( } private val glideRequests by lazy { - GlideApp.with(fragment) + GlideApp.with(editText) } - fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) { - this.fragment = fragment + fun setup(editText: EditText, listener: AutoCompleterListener) { + this.editText = editText - val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background)) + val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background)) setupCommands(backgroundDrawable, editText) setupUsers(backgroundDrawable, editText, listener) @@ -185,7 +185,7 @@ class AutoCompleter @Inject constructor( val span = PillImageSpan( glideRequests, avatarRenderer, - fragment.requireContext(), + editText.context, matrixItem ) span.bind(editText) 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 4ae79e8215..4414c48205 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 @@ -559,7 +559,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupComposer() { - autoCompleter.setup(this, composerLayout.composerEditText, this) + autoCompleter.setup(composerLayout.composerEditText, this) composerLayout.callback = object : TextComposerView.Callback { override fun onAddAttachment() { From 8b4c51139df1406d15fa2e6e76eca58eae7e4020 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 08:00:02 +0100 Subject: [PATCH 075/105] Completion on emoji WIP --- .../emoji/AutocompleteEmojiController.kt | 71 +++++++++++++++++++ .../emoji/AutocompleteEmojiItem.kt | 52 ++++++++++++++ .../emoji/AutocompleteEmojiPresenter.kt | 54 ++++++++++++++ .../home/room/detail/AutoCompleter.kt | 27 ++++++- .../reactions/EmojiSearchResultViewModel.kt | 16 +---- .../reactions/data/EmojiDataSource.kt | 21 ++++++ 6 files changed, 224 insertions(+), 17 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt new file mode 100644 index 0000000000..e0d7bdd888 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.emoji + +import android.graphics.Typeface +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.reactions.ReactionClickListener +import im.vector.riotx.features.reactions.data.EmojiItem +import im.vector.riotx.features.reactions.emojiSearchResultItem +import javax.inject.Inject + +class AutocompleteEmojiController @Inject constructor( + private val fontProvider: EmojiCompatFontProvider +) : TypedEpoxyController>() { + + var emojiTypeface: Typeface? = fontProvider.typeface + + private val fontProviderListener = object : EmojiCompatFontProvider.FontProviderListener { + override fun compatibilityFontUpdate(typeface: Typeface?) { + emojiTypeface = typeface + } + } + + init { + fontProvider.addListener(fontProviderListener) + } + + var listener: AutocompleteClickListener? = null + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { emojiItem -> + emojiSearchResultItem { + id(emojiItem.name) + emojiItem(emojiItem) + emojiTypeFace(emojiTypeface) + //currentQuery(data.query) + onClickListener(object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + listener?.onItemClick(reaction) + } + } + ) + } + } + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + fontProvider.removeListener(fontProviderListener) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt new file mode 100644 index 0000000000..e5c53315ab --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.emoji + +import android.view.View +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +//@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji) +//abstract class AutocompleteEmojiItem : VectorEpoxyModel() { +// +// @EpoxyAttribute +// var name: CharSequence? = null +// @EpoxyAttribute +// var parameters: CharSequence? = null +// @EpoxyAttribute +// var description: CharSequence? = null +// @EpoxyAttribute +// var clickListener: View.OnClickListener? = null +// +// override fun bind(holder: Holder) { +// holder.view.setOnClickListener(clickListener) +// +// holder.nameView.text = name +// holder.parametersView.text = parameters +// holder.descriptionView.text = description +// } +// +// class Holder : VectorEpoxyHolder() { +// val nameView by bind(R.id.commandName) +// val parametersView by bind(R.id.commandParameter) +// val descriptionView by bind(R.id.commandDescription) +// } +//} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt new file mode 100644 index 0000000000..731b48af86 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.emoji + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import com.otaliastudios.autocomplete.RecyclerViewPresenter +import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.reactions.data.EmojiDataSource +import javax.inject.Inject + +class AutocompleteEmojiPresenter @Inject constructor(context: Context, + private val emojiDataSource: EmojiDataSource, + private val controller: AutocompleteEmojiController) : + RecyclerViewPresenter(context), AutocompleteClickListener { + + init { + controller.listener = this + } + + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + // Also remove animation + recyclerView?.itemAnimator = null + return controller.adapter + } + + override fun onItemClick(t: String) { + dispatchClick(t) + } + + override fun onQuery(query: CharSequence?) { + val data = if (query.isNullOrBlank()) { + // Return common emojis + emojiDataSource.getQuickReactions() + } else { + emojiDataSource.filterWith(query.toString()) + } + controller.setData(data) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index ba643e2d7d..9fe2249450 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -21,7 +21,6 @@ import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable import android.widget.EditText -import androidx.fragment.app.Fragment import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy @@ -35,6 +34,7 @@ import im.vector.riotx.R import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy +import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter @@ -51,7 +51,8 @@ class AutoCompleter @Inject constructor( private val autocompleteCommandPresenter: AutocompleteCommandPresenter, private val autocompleteUserPresenter: AutocompleteUserPresenter, private val autocompleteRoomPresenter: AutocompleteRoomPresenter, - private val autocompleteGroupPresenter: AutocompleteGroupPresenter + private val autocompleteGroupPresenter: AutocompleteGroupPresenter, + private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter ) { private lateinit var editText: EditText @@ -76,6 +77,7 @@ class AutoCompleter @Inject constructor( setupUsers(backgroundDrawable, editText, listener) setupRooms(backgroundDrawable, editText, listener) setupGroups(backgroundDrawable, editText, listener) + setupEmojis(backgroundDrawable, editText) } fun render(state: TextComposerViewState) { @@ -162,6 +164,27 @@ class AutoCompleter @Inject constructor( .build() } + private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) { + Autocomplete.on(editText) + .with(CharPolicy(':', true)) + .with(autocompleteEmojiPresenter) + .with(ELEVATION) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: String): Boolean { + editable.clear() + editable + .append(item) + .append(" ") + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + } + private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) { // Detect last firstChar and remove it var startIndex = editable.lastIndexOf(firstChar) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt index 01debac5ed..aa5e79ed29 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt @@ -56,26 +56,12 @@ class EmojiSearchResultViewModel @AssistedInject constructor( } private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { - val words = action.queryString.split("\\s".toRegex()) setState { copy( query = action.queryString, // First add emojis with name matching query, sorted by name // Then emojis with keyword matching any of the word in the query, sorted by name - results = dataSource.rawData.emojis - .values - .filter { emojiItem -> - emojiItem.name.contains(action.queryString, true) - } - .sortedBy { it.name } - + dataSource.rawData.emojis - .values - .filter { emojiItem -> - words.fold(true, { prev, word -> - prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } - }) - } - .sortedBy { it.name } + results = dataSource.filterWith(action.queryString) ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index a326828112..873ab90254 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -33,4 +33,25 @@ class EmojiDataSource @Inject constructor( .fromJson(input.bufferedReader().use { it.readText() }) } ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + + fun filterWith(query: String): List { + val words = query.split("\\s".toRegex()) + + return rawData.emojis.values + .filter { emojiItem -> + emojiItem.name.contains(query, true) + } + .sortedBy { it.name } + + rawData.emojis.values + .filter { emojiItem -> + words.fold(true, { prev, word -> + prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } + }) + } + .sortedBy { it.name } + } + + fun getQuickReactions(): List { + return listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀").mapNotNull { rawData.emojis[it] } + } } From 9e73e95f55f1206bb2387831e740bab37635dff3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 08:36:54 +0100 Subject: [PATCH 076/105] Ensure there is never twice the same emoji --- .../vector/riotx/features/reactions/data/EmojiDataSource.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 873ab90254..8abea21667 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -37,7 +37,7 @@ class EmojiDataSource @Inject constructor( fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) - return rawData.emojis.values + return (rawData.emojis.values .filter { emojiItem -> emojiItem.name.contains(query, true) } @@ -48,7 +48,8 @@ class EmojiDataSource @Inject constructor( prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } }) } - .sortedBy { it.name } + .sortedBy { it.name }) + .distinct() } fun getQuickReactions(): List { From 5fa2acf60b197a945e68f370c1e2d9585d63826d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 08:46:55 +0100 Subject: [PATCH 077/105] Completion on emoji --- .../riotx/features/reactions/data/EmojiDataSource.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 8abea21667..b8ab24da4e 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -53,6 +53,15 @@ class EmojiDataSource @Inject constructor( } fun getQuickReactions(): List { - return listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀").mapNotNull { rawData.emojis[it] } + return listOf( + "+1", // 👍 + "-1", // 👎 + "grinning", // 😄 + "tada", // 🎉 + "confused", // 😕 + "heart", // ❤️ + "rocket", // 🚀 + "eyes" // 👀 + ).mapNotNull { rawData.emojis[it] } } } From c8e67f8ab4d6781a0e7833c71b04b3491e55a9cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 09:06:28 +0100 Subject: [PATCH 078/105] Completion on emoji WIP --- .../emoji/AutocompleteEmojiController.kt | 15 +++-- .../emoji/AutocompleteEmojiItem.kt | 60 ++++++++++--------- .../home/room/detail/AutoCompleter.kt | 18 ++++-- .../res/layout/item_autocomplete_emoji.xml | 56 +++++++++++++++++ 4 files changed, 110 insertions(+), 39 deletions(-) create mode 100644 vector/src/main/res/layout/item_autocomplete_emoji.xml diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt index e0d7bdd888..bef43d8b14 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -23,7 +23,6 @@ import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.reactions.ReactionClickListener import im.vector.riotx.features.reactions.data.EmojiItem -import im.vector.riotx.features.reactions.emojiSearchResultItem import javax.inject.Inject class AutocompleteEmojiController @Inject constructor( @@ -49,16 +48,16 @@ class AutocompleteEmojiController @Inject constructor( return } data.forEach { emojiItem -> - emojiSearchResultItem { + autocompleteEmojiItem { id(emojiItem.name) emojiItem(emojiItem) emojiTypeFace(emojiTypeface) - //currentQuery(data.query) - onClickListener(object : ReactionClickListener { - override fun onReactionSelected(reaction: String) { - listener?.onItemClick(reaction) - } - } + onClickListener( + object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + listener?.onItemClick(reaction) + } + } ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt index e5c53315ab..36759f9271 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt @@ -16,37 +16,43 @@ package im.vector.riotx.features.autocomplete.emoji -import android.view.View +import android.graphics.Typeface import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.reactions.ReactionClickListener +import im.vector.riotx.features.reactions.data.EmojiItem -//@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji) -//abstract class AutocompleteEmojiItem : VectorEpoxyModel() { -// -// @EpoxyAttribute -// var name: CharSequence? = null -// @EpoxyAttribute -// var parameters: CharSequence? = null -// @EpoxyAttribute -// var description: CharSequence? = null -// @EpoxyAttribute -// var clickListener: View.OnClickListener? = null -// -// override fun bind(holder: Holder) { -// holder.view.setOnClickListener(clickListener) -// -// holder.nameView.text = name -// holder.parametersView.text = parameters -// holder.descriptionView.text = description -// } -// -// class Holder : VectorEpoxyHolder() { -// val nameView by bind(R.id.commandName) -// val parametersView by bind(R.id.commandParameter) -// val descriptionView by bind(R.id.commandDescription) -// } -//} +@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji) +abstract class AutocompleteEmojiItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var emojiItem: EmojiItem + + @EpoxyAttribute + var emojiTypeFace: Typeface? = null + + @EpoxyAttribute + var onClickListener: ReactionClickListener? = null + + override fun bind(holder: Holder) { + holder.emojiText.text = emojiItem.emoji + holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT + holder.emojiNameText.text = emojiItem.name + holder.emojiKeywordText.setTextOrHide(emojiItem.keywords.joinToString()) + + holder.view.setOnClickListener { + onClickListener?.onReactionSelected(emojiItem.emoji) + } + } + + class Holder : VectorEpoxyHolder() { + val emojiText by bind(R.id.itemAutocompleteEmoji) + val emojiNameText by bind(R.id.itemAutocompleteEmojiName) + val emojiKeywordText by bind(R.id.itemAutocompleteEmojiSubname) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 9fe2249450..78ddf80a84 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -172,10 +172,20 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: String): Boolean { - editable.clear() - editable - .append(item) - .append(" ") + // Detect last ":" and remove it + var startIndex = editable.lastIndexOf(":") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + editable.replace(startIndex, endIndex, item) return true } diff --git a/vector/src/main/res/layout/item_autocomplete_emoji.xml b/vector/src/main/res/layout/item_autocomplete_emoji.xml new file mode 100644 index 0000000000..650a405f34 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_emoji.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + \ No newline at end of file From 0e5fcd071cffd4621101f33947b09fd92ea491c8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 09:21:56 +0100 Subject: [PATCH 079/105] Completion on emoji: display the first 50 results --- .../emoji/AutocompleteEmojiController.kt | 36 ++++++++++++------- .../emoji/AutocompleteMoreResultItem.kt | 28 +++++++++++++++ .../home/room/detail/AutoCompleter.kt | 2 +- .../layout/item_autocomplete_more_result.xml | 9 +++++ vector/src/main/res/values/strings_riotX.xml | 2 ++ 5 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt create mode 100644 vector/src/main/res/layout/item_autocomplete_more_result.xml diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt index bef43d8b14..010b362b68 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -47,18 +47,26 @@ class AutocompleteEmojiController @Inject constructor( if (data.isNullOrEmpty()) { return } - data.forEach { emojiItem -> - autocompleteEmojiItem { - id(emojiItem.name) - emojiItem(emojiItem) - emojiTypeFace(emojiTypeface) - onClickListener( - object : ReactionClickListener { - override fun onReactionSelected(reaction: String) { - listener?.onItemClick(reaction) - } - } - ) + data + .take(MAX) + .forEach { emojiItem -> + autocompleteEmojiItem { + id(emojiItem.name) + emojiItem(emojiItem) + emojiTypeFace(emojiTypeface) + onClickListener( + object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + listener?.onItemClick(reaction) + } + } + ) + } + } + + if (data.size > MAX) { + autocompleteMoreResultItem { + id("more_result") } } } @@ -67,4 +75,8 @@ class AutocompleteEmojiController @Inject constructor( super.onDetachedFromRecyclerView(recyclerView) fontProvider.removeListener(fontProviderListener) } + + companion object { + const val MAX = 50 + } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt new file mode 100644 index 0000000000..844cc96035 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.autocomplete.emoji + +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_autocomplete_more_result) +abstract class AutocompleteMoreResultItem : VectorEpoxyModel() { + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 78ddf80a84..609e7e2183 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -166,7 +166,7 @@ class AutoCompleter @Inject constructor( private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) - .with(CharPolicy(':', true)) + .with(CharPolicy(':', false)) .with(autocompleteEmojiPresenter) .with(ELEVATION) .with(backgroundDrawable) diff --git a/vector/src/main/res/layout/item_autocomplete_more_result.xml b/vector/src/main/res/layout/item_autocomplete_more_result.xml new file mode 100644 index 0000000000..d04f515ed0 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_more_result.xml @@ -0,0 +1,9 @@ + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 1502740112..317511b921 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -18,4 +18,6 @@ Current device Other devices + Limited results, please type more letters… + From 92d7ebe94f6be8f0fcb25eba4182da1aefaaa05c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 09:22:43 +0100 Subject: [PATCH 080/105] Update changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 560350dddf..b11be910db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ Improvements 🙌: - Introduce developer mode in the settings (#796) - Improve devices list screen - Add settings for rageshake sensibility + - Fix autocompletion issues and add support for rooms, groups, and emoji (#780) Other changes: - From 9ecceafb960d2155f423c0a40099b8bb0d37aa64 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 21:56:35 +0100 Subject: [PATCH 081/105] Move comment --- .../riotx/features/reactions/EmojiSearchResultViewModel.kt | 2 -- .../im/vector/riotx/features/reactions/data/EmojiDataSource.kt | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt index aa5e79ed29..8aa03d9b22 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt @@ -59,8 +59,6 @@ class EmojiSearchResultViewModel @AssistedInject constructor( setState { copy( query = action.queryString, - // First add emojis with name matching query, sorted by name - // Then emojis with keyword matching any of the word in the query, sorted by name results = dataSource.filterWith(action.queryString) ) } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index b8ab24da4e..8a279a7d4d 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -37,11 +37,13 @@ class EmojiDataSource @Inject constructor( fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) + // First add emojis with name matching query, sorted by name return (rawData.emojis.values .filter { emojiItem -> emojiItem.name.contains(query, true) } .sortedBy { it.name } + + // Then emojis with keyword matching any of the word in the query, sorted by name rawData.emojis.values .filter { emojiItem -> words.fold(true, { prev, word -> @@ -49,6 +51,7 @@ class EmojiDataSource @Inject constructor( }) } .sortedBy { it.name }) + // and ensure they will not be present twice .distinct() } From 448552d287e79f1d4035ef918668c504a9d9838f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 10:42:01 +0100 Subject: [PATCH 082/105] Move list of Quick Emoji to Emoji Data Source --- .../action/MessageActionsViewModel.kt | 6 ++-- .../reactions/data/EmojiDataSource.kt | 31 +++++++++++++------ .../res/layout/item_autocomplete_emoji.xml | 2 +- vector/src/main/res/values/strings_riotX.xml | 2 +- 4 files changed, 25 insertions(+), 16 deletions(-) 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 d08a891081..aad73e12f4 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 @@ -43,6 +43,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformatio import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.settings.VectorPreferences +import im.vector.riotx.features.reactions.data.EmojiDataSource import java.text.SimpleDateFormat import java.util.* @@ -101,9 +102,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } companion object : MvRxViewModelFactory { - - val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀") - @JvmStatic override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() @@ -161,7 +159,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted RxRoom(room) .liveAnnotationSummary(eventId) .map { annotations -> - quickEmojis.map { emoji -> + EmojiDataSource.quickEmojis.map { emoji -> ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 8a279a7d4d..9317c645c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -34,6 +34,8 @@ class EmojiDataSource @Inject constructor( } ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + private val quickReactions = mutableListOf() + fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) @@ -56,15 +58,24 @@ class EmojiDataSource @Inject constructor( } fun getQuickReactions(): List { - return listOf( - "+1", // 👍 - "-1", // 👎 - "grinning", // 😄 - "tada", // 🎉 - "confused", // 😕 - "heart", // ❤️ - "rocket", // 🚀 - "eyes" // 👀 - ).mapNotNull { rawData.emojis[it] } + if (quickReactions.isEmpty()) { + listOf( + "+1", // 👍 + "-1", // 👎 + "grinning", // 😄 + "tada", // 🎉 + "confused", // 😕 + "heart", // ❤️ + "rocket", // 🚀 + "eyes" // 👀 + ) + .mapNotNullTo(quickReactions) { rawData.emojis[it] } + } + + return quickReactions + } + + companion object { + val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀") } } diff --git a/vector/src/main/res/layout/item_autocomplete_emoji.xml b/vector/src/main/res/layout/item_autocomplete_emoji.xml index 650a405f34..c34ab0d452 100644 --- a/vector/src/main/res/layout/item_autocomplete_emoji.xml +++ b/vector/src/main/res/layout/item_autocomplete_emoji.xml @@ -53,4 +53,4 @@ - \ No newline at end of file + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 317511b921..3e8485ebcc 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -18,6 +18,6 @@ Current device Other devices - Limited results, please type more letters… + Showing only the first results, type more letters… From 3cc15387ae3ec1dca308a2070eabca65bbd73551 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 6 Jan 2020 18:41:09 +0100 Subject: [PATCH 083/105] Realm: compatch on launch --- .../internal/database/SessionRealmConfigurationFactory.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt index bc806a56a4..24f8a83c56 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt @@ -57,6 +57,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val .apply() val realmConfiguration = RealmConfiguration.Builder() + .compactOnLaunch() .directory(directory) .name(REALM_NAME) .apply { From 99c523b710bec38d3ce80515530925761cb1c6e5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 6 Jan 2020 18:43:34 +0100 Subject: [PATCH 084/105] Update libs --- matrix-sdk-android/build.gradle | 5 ++--- vector/build.gradle | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 799a619748..7a1348a54c 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -102,7 +102,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.recyclerview:recyclerview:1.1.0-beta05" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" @@ -119,14 +118,14 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.0.0' + implementation 'androidx.exifinterface:exifinterface:1.1.0' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1' // Work - implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01" + implementation "androidx.work:work-runtime-ktx:2.3.0-beta02" // FP implementation "io.arrow-kt:arrow-core:$arrow_version" diff --git a/vector/build.gradle b/vector/build.gradle index 410e478ee2..461168a7a5 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -236,6 +236,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + implementation "androidx.recyclerview:recyclerview:1.2.0-alpha01" implementation 'androidx.appcompat:appcompat:1.1.0' implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version" From f9487f89959f0fee398ce31357035d97146c2e8f Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 6 Jan 2020 18:44:04 +0100 Subject: [PATCH 085/105] Work on timeline --- .../api/session/room/model/RoomSummary.kt | 3 +- .../database/helper/ChunkEntityHelper.kt | 4 +- .../database/mapper/RoomSummaryMapper.kt | 3 +- .../database/model/RoomSummaryEntity.kt | 3 +- .../session/room/RoomSummaryUpdater.kt | 10 +- .../session/room/timeline/DefaultTimeline.kt | 52 +++++----- .../home/room/detail/RoomDetailFragment.kt | 38 ++++--- .../home/room/detail/RoomDetailViewModel.kt | 99 ++++++++++--------- .../home/room/detail/RoomDetailViewState.kt | 2 - .../timeline/TimelineEventController.kt | 7 +- 10 files changed, 112 insertions(+), 109 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 129c35a17e..c18645ddbd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -41,7 +41,8 @@ data class RoomSummary( val membership: Membership = Membership.NONE, val versioningState: VersioningState = VersioningState.NONE, val readMarkerId: String? = null, - val userDrafts: List = emptyList() + val userDrafts: List = emptyList(), + var isEncrypted: Boolean ) { val isVersioned: Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 4060e21102..3fa355fe3c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -92,7 +92,7 @@ internal fun ChunkEntity.add(roomId: String, } } - val isUnlinked = isUnlinked + val isChunkUnlinked = isUnlinked val localId = TimelineEventEntity.nextId(realm) val eventId = event.eventId ?: "" val senderId = event.senderId ?: "" @@ -121,7 +121,7 @@ internal fun ChunkEntity.add(roomId: String, this.stateIndex = currentStateIndex this.displayIndex = currentDisplayIndex this.sendState = SendState.SYNCED - this.isUnlinked = isUnlinked + this.isUnlinked = isChunkUnlinked } val eventEntity = realm.createObject().also { it.localId = localId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index eeb340eacb..7d25a846ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -70,7 +70,8 @@ internal class RoomSummaryMapper @Inject constructor( readMarkerId = roomSummaryEntity.readMarkerId, userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(), canonicalAlias = roomSummaryEntity.canonicalAlias, - aliases = roomSummaryEntity.aliases.toList() + aliases = roomSummaryEntity.aliases.toList(), + isEncrypted = roomSummaryEntity.isEncrypted ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 2fa892d874..4c99832b39 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -43,7 +43,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var canonicalAlias: String? = null, var aliases: RealmList = RealmList(), // this is required for querying - var flatAliases: String = "" + var flatAliases: String = "", + var isEncrypted: Boolean = false ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 536484de5a..30d9969f15 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 @@ -93,10 +93,12 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev() val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() + val encryptionEvent = EventEntity.where(realm, roomId, EventType.ENCRYPTION).prev() roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 - // avoid this call if we are sure there are unread events - || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) + // avoid this call if we are sure there are unread events + || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) + roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) @@ -105,10 +107,12 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel() ?.canonicalAlias - val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases ?: emptyList() + val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases + ?: emptyList() roomSummaryEntity.aliases.clear() roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") + roomSummaryEntity.isEncrypted = encryptionEvent != null if (updateMembers) { val otherRoomMembers = RoomMembers(realm, roomId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 85bab5d706..7835cf7e3e 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 @@ -52,8 +52,8 @@ import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort import timber.log.Timber -import java.util.Collections -import java.util.UUID +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList @@ -77,11 +77,9 @@ internal class DefaultTimeline( private val hiddenReadReceipts: TimelineHiddenReadReceipts ) : Timeline, TimelineHiddenReadReceipts.Delegate { - private companion object { - val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") - } + val backgroundHandler = createBackgroundHandler("TIMELINE_DB_THREAD_${System.currentTimeMillis()}") - private val listeners = ArrayList() + private val listeners = CopyOnWriteArrayList() private val isStarted = AtomicBoolean(false) private val isReady = AtomicBoolean(false) private val mainHandler = createUIHandler() @@ -137,7 +135,7 @@ internal class DefaultTimeline( // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { - BACKGROUND_HANDLER.post { + backgroundHandler.post { if (!canPaginate(direction)) { return@post } @@ -165,7 +163,7 @@ internal class DefaultTimeline( override fun start() { if (isStarted.compareAndSet(false, true)) { Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") - BACKGROUND_HANDLER.post { + backgroundHandler.post { eventDecryptor.start() val realm = Realm.getInstance(realmConfiguration) backgroundRealm.set(realm) @@ -199,8 +197,8 @@ internal class DefaultTimeline( isReady.set(false) Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") cancelableBag.cancel() - BACKGROUND_HANDLER.removeCallbacksAndMessages(null) - BACKGROUND_HANDLER.post { + backgroundHandler.removeCallbacksAndMessages(null) + backgroundHandler.post { roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() if (this::eventRelations.isInitialized) { eventRelations.removeAllChangeListeners() @@ -288,20 +286,20 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } - override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { + override fun addListener(listener: Timeline.Listener): Boolean { if (listeners.contains(listener)) { return false } - listeners.add(listener).also { + return listeners.add(listener).also { postSnapshot() } } - override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) { - listeners.remove(listener) + override fun removeListener(listener: Timeline.Listener): Boolean { + return listeners.remove(listener) } - override fun removeAllListeners() = synchronized(listeners) { + override fun removeAllListeners() { listeners.clear() } @@ -497,9 +495,9 @@ internal class DefaultTimeline( return } val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -516,7 +514,7 @@ internal class DefaultTimeline( } TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> // Database won't be updated, so we force pagination request - BACKGROUND_HANDLER.post { + backgroundHandler.post { executePaginationTask(direction, limit) } } @@ -575,7 +573,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } @@ -649,17 +647,15 @@ internal class DefaultTimeline( } private fun postSnapshot() { - BACKGROUND_HANDLER.post { + backgroundHandler.post { if (isReady.get().not()) { return@post } updateLoadingStates(filteredEvents) val snapshot = createSnapshot() val runnable = Runnable { - synchronized(listeners) { - listeners.forEach { - it.onTimelineUpdated(snapshot) - } + listeners.forEach { + it.onTimelineUpdated(snapshot) } } debouncer.debounce("post_snapshot", runnable, 50) @@ -671,10 +667,8 @@ internal class DefaultTimeline( return } val runnable = Runnable { - synchronized(listeners) { - listeners.forEach { - it.onTimelineFailure(throwable) - } + listeners.forEach { + it.onTimelineFailure(throwable) } } mainHandler.post(runnable) diff --git a/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 fa483d675c..8c985c27c0 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 @@ -40,6 +40,7 @@ import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach import androidx.core.view.isVisible +import androidx.lifecycle.observe import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -71,6 +72,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toRoomAliasMatrixItem +import im.vector.matrix.rx.rx import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer @@ -225,6 +227,8 @@ class RoomDetailFragment @Inject constructor( setupNotificationView() setupJumpToReadMarkerView() setupJumpToBottomView() + + roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } @@ -325,12 +329,10 @@ class RoomDetailFragment @Inject constructor( jumpToBottomView.setOnClickListener { roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) jumpToBottomView.visibility = View.INVISIBLE - withState(roomDetailViewModel) { state -> - if (state.timeline?.isLive == false) { - state.timeline.restartWithEventId(null) - } else { - layoutManager.scrollToPosition(0) - } + if (!roomDetailViewModel.timeline.isLive) { + roomDetailViewModel.timeline.restartWithEventId(null) + } else { + layoutManager.scrollToPosition(0) } } } @@ -343,9 +345,9 @@ class RoomDetailFragment @Inject constructor( AlertDialog.Builder(requireActivity()) .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.error_file_too_big, - error.filename, - TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), - TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) + error.filename, + TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), + TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) )) .setPositiveButton(R.string.ok, null) .show() @@ -431,7 +433,8 @@ class RoomDetailFragment @Inject constructor( composerLayout.sendButton.setContentDescription(getString(descriptionRes)) avatarRenderer.render( - MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), + MatrixItem.UserItem(event.root.senderId + ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), composerLayout.composerRelatedMessageAvatar ) composerLayout.expand { @@ -449,7 +452,7 @@ class RoomDetailFragment @Inject constructor( // Ignore update to avoid saving a draft composerLayout.composerEditText.setText(text) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length - ?: 0) + ?: 0) } } @@ -481,6 +484,9 @@ class RoomDetailFragment @Inject constructor( // PRIVATE METHODS ***************************************************************************** private fun setupRecyclerView() { + timelineEventController.callback = this + timelineEventController.timeline = roomDetailViewModel.timeline + val epoxyVisibilityTracker = EpoxyVisibilityTracker() epoxyVisibilityTracker.attach(recyclerView) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) @@ -514,8 +520,6 @@ class RoomDetailFragment @Inject constructor( } }) - timelineEventController.callback = this - if (vectorPreferences.swipeToReplyIsEnabled()) { val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { @@ -789,11 +793,12 @@ class RoomDetailFragment @Inject constructor( } private fun renderState(state: RoomDetailViewState) { + Timber.v("Render state summary complete: ${state.asyncRoomSummary.complete}") renderRoomSummary(state) val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { - scrollOnHighlightedEventCallback.timeline = state.timeline + scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline timelineEventController.update(state) inviteView.visibility = View.GONE val uid = session.myUserId @@ -808,9 +813,10 @@ class RoomDetailFragment @Inject constructor( } else if (state.asyncInviter.complete) { vectorBaseActivity.finish() } + val isRoomEncrypted = summary?.isEncrypted ?: false if (state.tombstoneEvent == null) { composerLayout.visibility = View.VISIBLE - composerLayout.setRoomEncrypted(state.isEncrypted) + composerLayout.setRoomEncrypted(isRoomEncrypted) notificationAreaView.render(NotificationAreaView.State.Hidden) } else { composerLayout.visibility = View.GONE @@ -1312,7 +1318,7 @@ class RoomDetailFragment @Inject constructor( val startToCompose = composerLayout.composerEditText.text.isNullOrBlank() if (startToCompose - && userId == session.myUserId) { + && userId == session.myUserId) { // Empty composer, current user: start an emote composerLayout.composerEditText.setText(Command.EMOTE.command + " ") composerLayout.composerEditText.setSelection(Command.EMOTE.length) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 467148302f..c93358a04e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -20,14 +20,18 @@ import android.net.Uri import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.PublishRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixPatterns -import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isImageMessage @@ -89,20 +93,21 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val visibleEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, - filterEdits = false, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = false, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } else { TimelineSettings(30, - filterEdits = true, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = true, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } private var timelineEvents = PublishRelay.create>() - private var timeline = room.createTimeline(eventId, timelineSettings) + var timeline = room.createTimeline(eventId, timelineSettings) + private set private val _viewEvents = PublishDataSource() val viewEvents: DataSource = _viewEvents @@ -138,18 +143,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } init { + timeline.start() + timeline.addListener(this) + observeRoomSummary() + observeSummaryState() getUnreadState() observeSyncState() - observeRoomSummary() observeEventDisplayedActions() - observeSummaryState() observeDrafts() observeUnreadState() + room.getRoomSummaryLive() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() - timeline.addListener(this) - timeline.start() - setState { copy(timeline = this@RoomDetailViewModel.timeline) } - // Inform the SDK that the room is displayed session.onRoomDisplayed(initialState.roomId) } @@ -233,23 +237,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro copy( // Create a sendMode from a draft and retrieve the TimelineEvent sendMode = when (draft) { - is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, draft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, draft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, draft.text) - } - } - } ?: SendMode.REGULAR("") + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") ) } } @@ -258,7 +262,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -310,7 +314,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro else -> false } - // PRIVATE METHODS ***************************************************************************** +// PRIVATE METHODS ***************************************************************************** private fun handleSendMessage(action: RoomDetailAction.SendMessage) { withState { state -> @@ -396,7 +400,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.EDIT -> { // is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -405,13 +409,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", - messageContent?.type ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -422,7 +426,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) @@ -538,7 +542,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { null -> room.sendMedias(attachments) else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name - ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) + ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) } } } @@ -728,7 +732,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event - ?: return@subscribeBy + ?: return@subscribeBy val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent if (trackUnreadMessages.get()) { if (globalMostRecentDisplayedEvent == null) { @@ -791,10 +795,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro room.rx().liveRoomSummary() .unwrap() .execute { async -> - copy( - asyncRoomSummary = async, - isEncrypted = room.isEncrypted() - ) + copy(asyncRoomSummary = async) } } @@ -880,7 +881,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro override fun onCleared() { timeline.dispose() - timeline.removeListener(this) + timeline.removeAllListeners() super.onCleared() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index b2ad29668e..3067b7f1b2 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 @@ -51,11 +51,9 @@ sealed class UnreadState { data class RoomDetailViewState( val roomId: String, val eventId: String?, - val timeline: Timeline? = null, val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, val sendMode: SendMode = SendMode.REGULAR(""), - val isEncrypted: Boolean = false, val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.Idle, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 582544ce8a..a08669da3b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -95,12 +95,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val modelCache = arrayListOf() private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false - private var timeline: Timeline? = null private var unreadState: UnreadState = UnreadState.Unknown private var positionOfReadMarker: Int? = null private var eventIdToHighlight: String? = null var callback: Callback? = null + var timeline: Timeline? = null private val listUpdateCallback = object : ListUpdateCallback { @@ -176,10 +176,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } fun update(viewState: RoomDetailViewState) { - if (timeline?.timelineID != viewState.timeline?.timelineID) { - timeline = viewState.timeline - timeline?.addListener(this) - } var requestModelBuild = false if (eventIdToHighlight != viewState.highlightedEventId) { // Clear cache to force a refresh @@ -205,6 +201,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) + timeline?.addListener(this) timelineMediaSizeProvider.recyclerView = recyclerView } From f09bf61750e934fe5bcd1320d15e1a31f187cfb7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jan 2020 13:31:34 +0100 Subject: [PATCH 086/105] Room detail: try to get some better perfs with fetching data. LiveData is slow as we only use one HandlerThread at the time. Might want Realm 7.0 and frozen objects to rework that --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 3 + .../session/room/timeline/DefaultTimeline.kt | 16 ++-- .../home/room/detail/RoomDetailFragment.kt | 20 ++-- .../home/room/detail/RoomDetailViewModel.kt | 1 + .../action/MessageActionsEpoxyController.kt | 2 +- .../action/MessageActionsViewModel.kt | 94 +++++++++---------- 6 files changed, 64 insertions(+), 72 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 bf4e924cf0..8a94fa30f7 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 @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import io.reactivex.Observable import io.reactivex.Single @@ -29,6 +30,7 @@ class RxRoom(private val room: Room) { fun liveRoomSummary(): Observable> { return room.getRoomSummaryLive().asObservable() + .startWith(room.roomSummary().toOptional()) } fun liveRoomMembers(memberships: List): Observable> { @@ -41,6 +43,7 @@ class RxRoom(private val room: Room) { fun liveTimelineEvent(eventId: String): Observable> { return room.getTimeLineEventLive(eventId).asObservable() + .startWith(room.getTimeLineEvent(eventId).toOptional()) } fun liveReadMarker(): Observable> { 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 7835cf7e3e..3e0f488d1a 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 @@ -77,7 +77,9 @@ internal class DefaultTimeline( private val hiddenReadReceipts: TimelineHiddenReadReceipts ) : Timeline, TimelineHiddenReadReceipts.Delegate { - val backgroundHandler = createBackgroundHandler("TIMELINE_DB_THREAD_${System.currentTimeMillis()}") + companion object{ + val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") + } private val listeners = CopyOnWriteArrayList() private val isStarted = AtomicBoolean(false) @@ -135,7 +137,7 @@ internal class DefaultTimeline( // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { - backgroundHandler.post { + BACKGROUND_HANDLER.post { if (!canPaginate(direction)) { return@post } @@ -163,7 +165,7 @@ internal class DefaultTimeline( override fun start() { if (isStarted.compareAndSet(false, true)) { Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") - backgroundHandler.post { + BACKGROUND_HANDLER.post { eventDecryptor.start() val realm = Realm.getInstance(realmConfiguration) backgroundRealm.set(realm) @@ -197,8 +199,8 @@ internal class DefaultTimeline( isReady.set(false) Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") cancelableBag.cancel() - backgroundHandler.removeCallbacksAndMessages(null) - backgroundHandler.post { + BACKGROUND_HANDLER.removeCallbacksAndMessages(null) + BACKGROUND_HANDLER.post { roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() if (this::eventRelations.isInitialized) { eventRelations.removeAllChangeListeners() @@ -514,7 +516,7 @@ internal class DefaultTimeline( } TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> // Database won't be updated, so we force pagination request - backgroundHandler.post { + BACKGROUND_HANDLER.post { executePaginationTask(direction, limit) } } @@ -647,7 +649,7 @@ internal class DefaultTimeline( } private fun postSnapshot() { - backgroundHandler.post { + BACKGROUND_HANDLER.post { if (isReady.get().not()) { return@post } 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 8c985c27c0..d8502a75b6 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 @@ -40,7 +40,6 @@ import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach import androidx.core.view.isVisible -import androidx.lifecycle.observe import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -72,7 +71,6 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toRoomAliasMatrixItem -import im.vector.matrix.rx.rx import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer @@ -89,8 +87,8 @@ import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter -import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.autocomplete.member.AutocompleteMemberPresenter +import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.getColorFromUserId @@ -228,10 +226,9 @@ class RoomDetailFragment @Inject constructor( setupJumpToReadMarkerView() setupJumpToBottomView() - roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } - roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } + roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) } roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair -> val message = requireContext().getString(pair.first, *pair.second.toTypedArray()) @@ -345,9 +342,9 @@ class RoomDetailFragment @Inject constructor( AlertDialog.Builder(requireActivity()) .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.error_file_too_big, - error.filename, - TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), - TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) + error.filename, + TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), + TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) )) .setPositiveButton(R.string.ok, null) .show() @@ -434,7 +431,7 @@ class RoomDetailFragment @Inject constructor( avatarRenderer.render( MatrixItem.UserItem(event.root.senderId - ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), + ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), composerLayout.composerRelatedMessageAvatar ) composerLayout.expand { @@ -452,7 +449,7 @@ class RoomDetailFragment @Inject constructor( // Ignore update to avoid saving a draft composerLayout.composerEditText.setText(text) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length - ?: 0) + ?: 0) } } @@ -793,7 +790,6 @@ class RoomDetailFragment @Inject constructor( } private fun renderState(state: RoomDetailViewState) { - Timber.v("Render state summary complete: ${state.asyncRoomSummary.complete}") renderRoomSummary(state) val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() @@ -1318,7 +1314,7 @@ class RoomDetailFragment @Inject constructor( val startToCompose = composerLayout.composerEditText.text.isNullOrBlank() if (startToCompose - && userId == session.myUserId) { + && userId == session.myUserId) { // Empty composer, current user: start an emote composerLayout.composerEditText.setText(Command.EMOTE.command + " ") composerLayout.composerEditText.setSelection(Command.EMOTE.length) 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 c93358a04e..d4145a7de2 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 @@ -51,6 +51,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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 939564e780..9a2fb4b6de 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -93,7 +93,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid } // Action - state.actions()?.forEachIndexed { index, action -> + state.actions.forEachIndexed { index, action -> if (action is EventSharedAction.Separator) { bottomSheetSeparatorItem { id("separator_$index") diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index d537b66ec3..6bf5746735 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -31,8 +31,7 @@ import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited -import im.vector.matrix.android.api.util.Optional -import im.vector.matrix.rx.RxRoom +import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.R import im.vector.riotx.core.extensions.canReact @@ -62,7 +61,7 @@ data class MessageActionState( // For quick reactions val quickStates: Async> = Uninitialized, // For actions - val actions: Async> = Uninitialized, + val actions: List = emptyList(), val expendedReportContentMenu: Boolean = false ) : MvRxState { @@ -112,7 +111,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted init { observeEvent() observeReactions() - observeEventAction() + observeTimelineEventState() } override fun handle(action: MessageActionsAction) { @@ -131,32 +130,17 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun observeEvent() { if (room == null) return - RxRoom(room) + room.rx() .liveTimelineEvent(eventId) .unwrap() .execute { - copy( - timelineEvent = it, - messageBody = computeMessageBody(it) - ) - } - } - - private fun observeEventAction() { - if (room == null) return - RxRoom(room) - .liveTimelineEvent(eventId) - .map { - actionsForEvent(it) - } - .execute { - copy(actions = it) + copy(timelineEvent = it) } } private fun observeReactions() { if (room == null) return - RxRoom(room) + room.rx() .liveAnnotationSummary(eventId) .map { annotations -> quickEmojis.map { emoji -> @@ -168,11 +152,19 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - private fun computeMessageBody(timelineEvent: Async): CharSequence? { - return when (timelineEvent()?.root?.getClearType()) { + private fun observeTimelineEventState() { + asyncSubscribe(MessageActionState::timelineEvent) { timelineEvent -> + val computedMessage = computeMessageBody(timelineEvent) + val actions = actionsForEvent(timelineEvent) + setState { copy(messageBody = computedMessage, actions = actions) } + } + } + + private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence? { + return when (timelineEvent.root.getClearType()) { EventType.MESSAGE, EventType.STICKER -> { - val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent() + val messageContent: MessageContent? = timelineEvent.getLastMessageContent() if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val html = messageContent.formattedBody ?.takeIf { it.isNotBlank() } @@ -193,41 +185,39 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> { - timelineEvent()?.let { noticeEventFormatter.format(it) } + noticeEventFormatter.format(timelineEvent) } else -> null } } - private fun actionsForEvent(optionalEvent: Optional): List { - val event = optionalEvent.getOrNull() ?: return emptyList() - - val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel() - ?: event.root.getClearContent().toModel() + private fun actionsForEvent(timelineEvent: TimelineEvent): List { + val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() + ?: timelineEvent.root.getClearContent().toModel() val type = messageContent?.type return arrayListOf().apply { - if (event.root.sendState.hasFailed()) { - if (canRetry(event)) { + if (timelineEvent.root.sendState.hasFailed()) { + if (canRetry(timelineEvent)) { add(EventSharedAction.Resend(eventId)) } add(EventSharedAction.Remove(eventId)) - } else if (event.root.sendState.isSending()) { + } else if (timelineEvent.root.sendState.isSending()) { // TODO is uploading attachment? - if (canCancel(event)) { + if (canCancel(timelineEvent)) { add(EventSharedAction.Cancel(eventId)) } - } else if (event.root.sendState == SendState.SYNCED) { - if (!event.root.isRedacted()) { - if (canReply(event, messageContent)) { + } else if (timelineEvent.root.sendState == SendState.SYNCED) { + if (!timelineEvent.root.isRedacted()) { + if (canReply(timelineEvent, messageContent)) { add(EventSharedAction.Reply(eventId)) } - if (canEdit(event, session.myUserId)) { + if (canEdit(timelineEvent, session.myUserId)) { add(EventSharedAction.Edit(eventId)) } - if (canRedact(event, session.myUserId)) { + if (canRedact(timelineEvent, session.myUserId)) { add(EventSharedAction.Delete(eventId)) } @@ -236,19 +226,19 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.Copy(messageContent!!.body)) } - if (event.canReact()) { + if (timelineEvent.canReact()) { add(EventSharedAction.AddReaction(eventId)) } - if (canQuote(event, messageContent)) { + if (canQuote(timelineEvent, messageContent)) { add(EventSharedAction.Quote(eventId)) } - if (canViewReactions(event)) { + if (canViewReactions(timelineEvent)) { add(EventSharedAction.ViewReactions(informationData)) } - if (event.hasBeenEdited()) { + if (timelineEvent.hasBeenEdited()) { add(EventSharedAction.ViewEditHistory(informationData)) } @@ -261,29 +251,29 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted // TODO } - if (event.root.sendState == SendState.SENT) { + if (timelineEvent.root.sendState == SendState.SENT) { // TODO Can be redacted // TODO sent by me or sufficient power level } } - add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent())) - if (event.isEncrypted()) { - val decryptedContent = event.root.toClearContentStringWithIndent() + add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent())) + if (timelineEvent.isEncrypted()) { + val decryptedContent = timelineEvent.root.toClearContentStringWithIndent() ?: stringProvider.getString(R.string.encryption_information_decryption_error) add(EventSharedAction.ViewDecryptedSource(decryptedContent)) } add(EventSharedAction.CopyPermalink(eventId)) - if (session.myUserId != event.root.senderId) { + if (session.myUserId != timelineEvent.root.senderId) { // not sent by me - if (event.root.getClearType() == EventType.MESSAGE) { - add(EventSharedAction.ReportContent(eventId, event.root.senderId)) + if (timelineEvent.root.getClearType() == EventType.MESSAGE) { + add(EventSharedAction.ReportContent(eventId, timelineEvent.root.senderId)) } add(EventSharedAction.Separator) - add(EventSharedAction.IgnoreUser(event.root.senderId)) + add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId)) } } } From d710106bbbea953ec888532dd2ab3c7d0a5b4201 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jan 2020 14:09:04 +0100 Subject: [PATCH 087/105] Clean code --- .../android/internal/session/room/RoomSummaryUpdater.kt | 1 - .../android/internal/session/room/membership/RoomMembers.kt | 6 ++++-- .../internal/session/room/timeline/DefaultTimeline.kt | 2 +- .../riotx/features/home/room/detail/RoomDetailViewModel.kt | 1 - .../riotx/features/home/room/detail/RoomDetailViewState.kt | 1 - 5 files changed, 5 insertions(+), 6 deletions(-) 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 30d9969f15..ea5c2e858c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -99,7 +99,6 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId // avoid this call if we are sure there are unread events || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) - roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel()?.topic diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index f2dd733978..e3775f5ade 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -71,11 +71,13 @@ internal class RoomMembers(private val realm: Realm, } fun queryJoinedRoomMembersEvent(): RealmQuery { - return queryRoomMembersEvent().equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + return queryRoomMembersEvent() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } fun queryInvitedRoomMembersEvent(): RealmQuery { - return queryRoomMembersEvent().equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + return queryRoomMembersEvent() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) } fun queryActiveRoomMembersEvent(): RealmQuery { 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 3e0f488d1a..d90cb60a15 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 @@ -77,7 +77,7 @@ internal class DefaultTimeline( private val hiddenReadReceipts: TimelineHiddenReadReceipts ) : Timeline, TimelineHiddenReadReceipts.Delegate { - companion object{ + companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") } 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 d4145a7de2..c93358a04e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -51,7 +51,6 @@ 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 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 3067b7f1b2..165ef7b625 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User From 5a7f4bed43e1ed6c8144609e677fc71898cbc4ef Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Jan 2020 14:24:26 +0100 Subject: [PATCH 088/105] ktlint --- .../im/vector/matrix/android/internal/di/StringQualifiers.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt index 7d2610903a..32649443db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -32,7 +32,6 @@ internal annotation class UserId @Retention(AnnotationRetention.RUNTIME) internal annotation class UserMd5 - /** * Used to inject the sessionId, which is defined as md5(userId|deviceId) */ From 38c198fe02c5bc9151e45cbcc65640b8f415a85a Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jan 2020 18:15:07 +0100 Subject: [PATCH 089/105] Rx: fetch first before returning live data results --- .../src/main/java/im/vector/matrix/rx/RxRoom.kt | 4 +++- .../api/session/room/members/MembershipService.kt | 8 ++++++++ .../session/room/model/relation/RelationService.kt | 14 +++++++++++++- .../room/membership/DefaultMembershipService.kt | 11 +++++++++++ .../room/relation/DefaultRelationService.kt | 11 ++++++++++- 5 files changed, 45 insertions(+), 3 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 8a94fa30f7..48ff66720f 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,10 +35,12 @@ class RxRoom(private val room: Room) { fun liveRoomMembers(memberships: List): Observable> { return room.getRoomMembersLive(memberships).asObservable() + .startWith(room.getRoomMembers(memberships)) } fun liveAnnotationSummary(eventId: String): Observable> { - return room.getEventSummaryLive(eventId).asObservable() + return room.getEventAnnotationsSummaryLive(eventId).asObservable() + .startWith(room.getEventAnnotationsSummary(eventId).toOptional()) } fun liveTimelineEvent(eventId: String): Observable> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index 12f0378af7..281697816c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -41,6 +41,14 @@ interface MembershipService { */ fun getRoomMember(userId: String): RoomMember? + + /** + * Return all the roomMembers of the room filtered by memberships + * @param memberships list of accepted memberships + * @return a roomMember list. + */ + fun getRoomMembers(memberships: List): List + /** * Return all the roomMembers of the room filtered by memberships * @param memberships list of accepted memberships diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index 7d8f2f0bc1..d13d7db773 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -108,5 +108,17 @@ interface RelationService { replyText: CharSequence, autoMarkdown: Boolean = false): Cancelable? - fun getEventSummaryLive(eventId: String): LiveData> + /** + * Get the current EventAnnotationsSummary + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the EventAnnotationsSummary found + */ + fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? + + /** + * Get the a LiveData EventAnnotationsSummary + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the LiveData of EventAnnotationsSummary + */ + fun getEventAnnotationsSummaryLive(eventId: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 2b89f86d3e..789b783e3f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -64,6 +64,17 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr return roomMemberEntity?.asDomain() } + override fun getRoomMembers(memberships: List): List { + return monarchy.fetchAllMappedSync( + { + RoomMembers(it, roomId).queryRoomMembersEvent() + }, + { + it.asDomain() + } + ) + } + override fun getRoomMembersLive(memberships: List): LiveData> { return monarchy.findAllMappedWithChanges( { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 8731045e14..180776ba8d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -215,7 +215,16 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) } - override fun getEventSummaryLive(eventId: String): LiveData> { + override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { + return monarchy.fetchCopyMap( + { EventAnnotationsSummaryEntity.where(it, eventId).findFirst() }, + { entity, _ -> + entity.asDomain() + } + ) + } + + override fun getEventAnnotationsSummaryLive(eventId: String): LiveData> { val liveData = monarchy.findAllMappedWithChanges( { EventAnnotationsSummaryEntity.where(it, eventId) }, { it.asDomain() } From 03c3c9ae57f2aea92a4ef9514d89c6417c05eee1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jan 2020 18:15:48 +0100 Subject: [PATCH 090/105] Timeline: clear unlinked should use new parameters --- .../internal/session/room/timeline/ClearUnlinkedEventsTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt index 04cf810fe4..d92320abf9 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 @@ -38,7 +38,7 @@ internal class DefaultClearUnlinkedEventsTask @Inject constructor(private val mo monarchy.awaitTransaction { localRealm -> val unlinkedChunks = ChunkEntity .where(localRealm, roomId = params.roomId) - .equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true) + .equalTo(ChunkEntityFields.IS_UNLINKED, true) .findAll() unlinkedChunks.forEach { it.deleteOnCascade() From 8ef5c60e2e3b490550e141e19946793bcf034d24 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 11:43:21 +0100 Subject: [PATCH 091/105] RageShake is enabled by default --- vector/src/main/res/xml/vector_settings_advanced_settings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml index 93ff104a81..11ca97870d 100644 --- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -32,6 +32,7 @@ android:title="@string/settings_rageshake"> From 2dd2a8db6ca21e4dc6106710ef95eacd62dc77e6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jan 2020 11:54:42 +0100 Subject: [PATCH 092/105] Emoji data source as singleton --- .../src/main/java/im/vector/riotx/core/di/VectorComponent.kt | 3 +++ .../im/vector/riotx/features/reactions/data/EmojiDataSource.kt | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index b78e291506..283b43a004 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -44,6 +44,7 @@ import im.vector.riotx.features.notifications.* import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler +import im.vector.riotx.features.reactions.data.EmojiDataSource import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.share.ShareRoomListDataSource @@ -124,6 +125,8 @@ interface VectorComponent { fun uiStateRepository(): UiStateRepository + fun emojiDataSource(): EmojiDataSource + @Component.Factory interface Factory { fun create(@BindsInstance context: Context): VectorComponent diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 9317c645c4..2917dce68a 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -20,8 +20,9 @@ import com.squareup.moshi.Moshi import im.vector.riotx.R import im.vector.riotx.core.di.ScreenScope import javax.inject.Inject +import javax.inject.Singleton -@ScreenScope +@Singleton class EmojiDataSource @Inject constructor( resources: Resources ) { From 9970d7ffa0ec666c6365fa6f91318d0d29e25259 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jan 2020 11:55:22 +0100 Subject: [PATCH 093/105] SDK: get some better queries --- .../java/im/vector/matrix/rx/RxSession.kt | 24 ++++--- .../matrix/android/api/session/Session.kt | 2 +- .../android/api/session/group/GroupService.kt | 8 ++- .../api/session/pushers/PushersService.kt | 2 +- .../matrix/android/api/session/room/Room.kt | 3 + .../android/api/session/room/RoomService.kt | 20 ++++-- .../session/room/RoomSummaryQueryParams.kt | 38 +++++++++++ .../api/session/room/model/Membership.kt | 4 ++ .../android/api/session/user/UserService.kt | 8 +-- .../internal/session/DefaultSession.kt | 2 +- .../session/group/DefaultGroupService.kt | 20 +++++- .../session/pushers/DefaultPusherService.kt | 2 +- .../session/room/DefaultRoomService.kt | 65 ++++++++++++++----- .../membership/DefaultMembershipService.kt | 21 +++++- .../internal/session/sync/job/SyncService.kt | 2 +- .../session/user/DefaultUserService.kt | 8 +-- .../java/im/vector/riotx/AppStateHandler.kt | 3 +- .../riotx/features/home/HomeDrawerFragment.kt | 2 +- .../detail/composer/TextComposerViewModel.kt | 3 +- .../roomdirectory/RoomDirectoryViewModel.kt | 5 +- .../roompreview/RoomPreviewViewModel.kt | 5 +- .../features/share/IncomingShareViewModel.kt | 3 +- 22 files changed, 192 insertions(+), 58 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index c9381b861d..c502e4d564 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 @@ -20,6 +20,7 @@ import androidx.paging.PagedList import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.pushers.Pusher +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState @@ -30,40 +31,43 @@ import io.reactivex.Single class RxSession(private val session: Session) { - fun liveRoomSummaries(): Observable> { - return session.liveRoomSummaries().asObservable() + fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable> { + return session.getRoomSummariesLive(queryParams).asObservable() + .startWith(session.getRoomSummaries(queryParams)) } fun liveGroupSummaries(): Observable> { - return session.liveGroupSummaries().asObservable() + return session.getGroupSummariesLive().asObservable() + .startWith(session.getGroupSummaries()) } fun liveBreadcrumbs(): Observable> { - return session.liveBreadcrumbs().asObservable() + return session.getBreadcrumbsLive().asObservable() + .startWith(session.getBreadcrumbs()) } fun liveSyncState(): Observable { - return session.syncState().asObservable() + return session.getSyncStateLive().asObservable() } fun livePushers(): Observable> { - return session.livePushers().asObservable() + return session.getPushersLive().asObservable() } fun liveUser(userId: String): Observable> { - return session.liveUser(userId).asObservable().distinctUntilChanged() + return session.getUserLive(userId).asObservable().distinctUntilChanged() } fun liveUsers(): Observable> { - return session.liveUsers().asObservable() + return session.getUsersLive().asObservable() } fun liveIgnoredUsers(): Observable> { - return session.liveIgnoredUsers().asObservable() + return session.getIgnoredUsersLive().asObservable() } fun livePagedUsers(filter: String? = null): Observable> { - return session.livePagedUsers(filter).asObservable() + return session.getPagedUsersLive(filter).asObservable() } fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index ab545dbce6..1c73d4c5d1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -107,7 +107,7 @@ interface Session : * This method allows to listen the sync state. * @return a [LiveData] of [SyncState]. */ - fun syncState(): LiveData + fun getSyncStateLive(): LiveData /** * This methods return true if an initial sync has been processed diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt index 2d55d0be57..b01bfa34ab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt @@ -38,9 +38,15 @@ interface GroupService { */ fun getGroupSummary(groupId: String): GroupSummary? + /** + * Get a list of group summaries.This list is a snapshot of the data. + * @return the list of [GroupSummary] + */ + fun getGroupSummaries(): List + /** * Get a live list of group summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of [GroupSummary] */ - fun liveGroupSummaries(): LiveData> + fun getGroupSummariesLive(): LiveData> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt index d082faa7c7..129bfa3011 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt @@ -58,7 +58,7 @@ interface PushersService { const val EVENT_ID_ONLY = "event_id_only" } - fun livePushers(): LiveData> + fun getPushersLive(): LiveData> fun pushers() : List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 90790a6ab0..3221c355e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -56,5 +56,8 @@ interface Room : */ fun getRoomSummaryLive(): LiveData> + /** + * A current snapshot of [RoomSummary] associated with the room + */ fun roomSummary(): RoomSummary? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index ba3b5ded78..f3167c8461 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -60,16 +60,28 @@ interface RoomService { fun getRoomSummary(roomIdOrAlias: String): RoomSummary? /** - * Get a live list of room summaries. This list is refreshed as soon as the data changes. - * @return the [LiveData] of [RoomSummary] + * Get a snapshot list of room summaries. + * @return the immutable list of [RoomSummary] */ - fun liveRoomSummaries(): LiveData> + fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List + + /** + * Get a live list of room summaries. This list is refreshed as soon as the data changes. + * @return the [LiveData] of List[RoomSummary] + */ + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> + + /** + * Get a snapshot list of Breadcrumbs + * @return the immutable list of [RoomSummary] + */ + fun getBreadcrumbs(): List /** * Get a live list of Breadcrumbs * @return the [LiveData] of [RoomSummary] */ - fun liveBreadcrumbs(): LiveData> + fun getBreadcrumbsLive(): LiveData> /** * Inform the Matrix SDK that a room is displayed. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt new file mode 100644 index 0000000000..00a17f083d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room + +import im.vector.matrix.android.api.session.room.model.Membership + +/** + * This class can be used to filter room summaries to use with: + * [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService] + */ +data class RoomSummaryQueryParams( + /** + * Set to true if you want only non null display name. True by default + */ + val filterDisplayName: Boolean = true, + /** + * Set to true if you want only non null canonical alias. False by default. + */ + val filterCanonicalAlias: Boolean = false, + /** + * Set the list of memberships you want to filter on. By default, all memberships. + */ + val memberships: List = Membership.all() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt index 93eb54fbd3..5182d2099e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt @@ -48,5 +48,9 @@ enum class Membership(val value: String) { fun activeMemberships(): List { return listOf(INVITE, JOIN) } + + fun all(): List{ + return values().asList() + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index 2a93a876f6..453400bc99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -50,25 +50,25 @@ interface UserService { * @param userId the userId to look for. * @return a LiveData of user with userId */ - fun liveUser(userId: String): LiveData> + fun getUserLive(userId: String): LiveData> /** * Observe a live list of users sorted alphabetically * @return a Livedata of users */ - fun liveUsers(): LiveData> + fun getUsersLive(): LiveData> /** * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. * @param filter the filter. It will look into userId and displayName. * @return a Livedata of users */ - fun livePagedUsers(filter: String? = null): LiveData> + fun getPagedUsersLive(filter: String? = null): LiveData> /** * Get list of ignored users */ - fun liveIgnoredUsers(): LiveData> + fun getIgnoredUsersLive(): LiveData> /** * Ignore users diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index d52379eb6e..b0bf70eb70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -156,7 +156,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se syncTaskSequencer.close() } - override fun syncState(): LiveData { + override fun getSyncStateLive(): LiveData { return getSyncThread().liveState() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt index 192c6fe40c..584b577443 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt @@ -26,6 +26,8 @@ import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.util.fetchCopyMap +import io.realm.Realm +import io.realm.RealmQuery import javax.inject.Inject internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService { @@ -41,10 +43,22 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon ) } - override fun liveGroupSummaries(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) }, + override fun getGroupSummaries(): List { + return monarchy.fetchAllMappedSync( + { groupSummariesQuery(it) }, { it.asDomain() } ) } + + + override fun getGroupSummariesLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { groupSummariesQuery(it) }, + { it.asDomain() } + ) + } + + private fun groupSummariesQuery(realm: Realm): RealmQuery { + return GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt index 8c7e9fb263..fcce69c2fc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt @@ -86,7 +86,7 @@ internal class DefaultPusherService @Inject constructor(private val context: Con .executeBy(taskExecutor) } - override fun livePushers(): LiveData> { + override fun getPushersLive(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> PusherEntity.where(realm) }, { it.asDomain() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index b53fa3ce33..19c347ba8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams @@ -41,6 +42,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopyMap import io.realm.Realm +import io.realm.RealmQuery import javax.inject.Inject internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, @@ -86,30 +88,63 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona }) } - override fun liveRoomSummaries(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm -> - RoomSummaryEntity.where(realm) - .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) - }, + override fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List { + return monarchy.fetchAllMappedSync( + { roomSummariesQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } - override fun liveBreadcrumbs(): LiveData> { + override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> { return monarchy.findAllMappedWithChanges( - { realm -> - RoomSummaryEntity.where(realm) - .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) - .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) - .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) - }, + { roomSummariesQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } + private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { + val query = RoomSummaryEntity.where(realm) + if (queryParams.filterCanonicalAlias) { + query.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + } + if (queryParams.filterCanonicalAlias) { + query.isNotEmpty(RoomSummaryEntityFields.CANONICAL_ALIAS) + } + val lastMembership = queryParams.memberships.lastOrNull() + query.beginGroup() + for (membership in queryParams.memberships) { + query.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, membership.name) + if (membership != lastMembership) { + query.or() + } + } + query.endGroup() + query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + return query + } + + override fun getBreadcrumbs(): List { + return monarchy.fetchAllMappedSync( + { breadcrumbsQuery(it) }, + { roomSummaryMapper.map(it) } + ) + } + + override fun getBreadcrumbsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { breadcrumbsQuery(it) }, + { roomSummaryMapper.map(it) } + ) + } + + private fun breadcrumbsQuery(realm: Realm): RealmQuery { + return RoomSummaryEntity.where(realm) + .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) + .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) + } + override fun onRoomDisplayed(roomId: String): Cancelable { return updateBreadcrumbsTask .configureWith(UpdateBreadcrumbsTask.Params(roomId)) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 789b783e3f..2f8ce763b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -26,6 +26,8 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask @@ -33,6 +35,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopied import io.realm.Realm +import io.realm.RealmQuery internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String, private val monarchy: Monarchy, @@ -67,7 +70,7 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr override fun getRoomMembers(memberships: List): List { return monarchy.fetchAllMappedSync( { - RoomMembers(it, roomId).queryRoomMembersEvent() + roomMembersQuery(it, memberships) }, { it.asDomain() @@ -78,7 +81,7 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr override fun getRoomMembersLive(memberships: List): LiveData> { return monarchy.findAllMappedWithChanges( { - RoomMembers(it, roomId).queryRoomMembersEvent() + roomMembersQuery(it, memberships) }, { it.asDomain() @@ -86,6 +89,20 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr ) } + private fun roomMembersQuery(realm: Realm, memberships: List): RealmQuery { + val query = RoomMembers(realm, roomId).queryRoomMembersEvent() + val lastMembership = memberships.lastOrNull() + query.beginGroup() + for (membership in memberships) { + query.equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, membership.name) + if (membership != lastMembership) { + query.or() + } + } + query.endGroup() + return query + } + override fun getNumberOfJoinedMembers(): Int { return Realm.getInstance(monarchy.realmConfiguration).use { RoomMembers(it, roomId).getNumberOfJoinedMembers() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt index 37bcc225c1..9fe3e38d36 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt @@ -112,7 +112,7 @@ abstract class SyncService : Service() { try { syncTask.execute(params) // Start sync if we were doing an initial sync and the syncThread is not launched yet - if (isInitialSync && session.syncState().value == SyncState.Idle) { + if (isInitialSync && session.getSyncStateLive().value == SyncState.Idle) { val isForeground = !backgroundDetectionObserver.isInBackground session.startSync(isForeground) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index d314c8d108..761c810b41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -70,7 +70,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona return userEntity.asDomain() } - override fun liveUser(userId: String): LiveData> { + override fun getUserLive(userId: String): LiveData> { val liveData = monarchy.findAllMappedWithChanges( { UserEntity.where(it, userId) }, { it.asDomain() } @@ -80,7 +80,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona } } - override fun liveUsers(): LiveData> { + override fun getUsersLive(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> realm.where(UserEntity::class.java) @@ -91,7 +91,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona ) } - override fun livePagedUsers(filter: String?): LiveData> { + override fun getPagedUsersLive(filter: String?): LiveData> { realmDataSourceFactory.updateQuery { realm -> val query = realm.where(UserEntity::class.java) if (filter.isNullOrEmpty()) { @@ -121,7 +121,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona .executeBy(taskExecutor) } - override fun liveIgnoredUsers(): LiveData> { + override fun getIgnoredUsersLive(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> realm.where(IgnoredUserEntity::class.java) diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt index cfbed0ee13..78a880854e 100644 --- a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import arrow.core.Option import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.rx.rx import im.vector.riotx.features.home.HomeRoomListDataSource @@ -65,7 +66,7 @@ class AppStateHandler @Inject constructor( sessionDataSource.observe() .observeOn(AndroidSchedulers.mainThread()) .switchMap { - it.orNull()?.rx()?.liveRoomSummaries() + it.orNull()?.rx()?.liveRoomSummaries(RoomSummaryQueryParams()) ?: Observable.just(emptyList()) } .throttleLast(300, TimeUnit.MILLISECONDS), diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index 6ff836e8c8..bc3bc2f9d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -40,7 +40,7 @@ class HomeDrawerFragment @Inject constructor( if (savedInstanceState == null) { replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java) } - session.liveUser(session.myUserId).observeK(this) { optionalUser -> + session.getUserLive(session.myUserId).observeK(viewLifecycleOwner) { optionalUser -> val user = optionalUser?.getOrNull() if (user != null) { avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt index a2c9787bdf..37e01fcacf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt @@ -25,6 +25,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary @@ -114,7 +115,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: private fun observeRoomsQuery() { Observable.combineLatest, Option, List>( - session.rx().liveRoomSummaries(), + session.rx().liveRoomSummaries(RoomSummaryQueryParams(filterCanonicalAlias = true)), roomsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), BiFunction { roomSummaries, query -> val filter = query.orNull() ?: "" diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index dcd64c6a46..cc57775028 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -24,6 +24,7 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsFilter import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams @@ -81,11 +82,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: private fun observeJoinedRooms() { session .rx() - .liveRoomSummaries() + .liveRoomSummaries(RoomSummaryQueryParams(memberships = listOf(Membership.JOIN))) .subscribe { list -> val joinedRoomIds = list - // Keep only joined room - ?.filter { it.membership == Membership.JOIN } ?.map { it.roomId } ?.toSet() ?: emptySet() diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index 54c86537d2..005a930fdc 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -23,6 +23,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.session.Session +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.VectorViewModel @@ -55,12 +56,10 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R private fun observeJoinedRooms() { session .rx() - .liveRoomSummaries() + .liveRoomSummaries(RoomSummaryQueryParams(memberships = listOf(Membership.JOIN))) .subscribe { list -> withState { state -> val isRoomJoined = list - // Keep only joined room - ?.filter { it.membership == Membership.JOIN } ?.map { it.roomId } ?.toList() ?.contains(state.roomId) == true diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt index 72c98cdc45..904407708f 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.rx.rx import im.vector.riotx.ActiveSessionDataSource import im.vector.riotx.core.platform.EmptyAction @@ -62,7 +63,7 @@ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: sessionObservableStore.observe() .observeOn(AndroidSchedulers.mainThread()) .switchMap { - it.orNull()?.rx()?.liveRoomSummaries() + it.orNull()?.rx()?.liveRoomSummaries(RoomSummaryQueryParams()) ?: Observable.just(emptyList()) } .throttleLast(300, TimeUnit.MILLISECONDS) From c60b4ddb5a2332de1ccfd10606b090b873ca7443 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jan 2020 13:59:43 +0100 Subject: [PATCH 094/105] Timeline: don't wait for realm notification to come back, use it right away to init --- .../session/room/timeline/DefaultTimeline.kt | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) 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 d90cb60a15..057295ec44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,13 +27,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.FilterContent import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where @@ -44,13 +38,7 @@ import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createUIHandler -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* import timber.log.Timber import java.util.* import java.util.concurrent.CopyOnWriteArrayList @@ -113,11 +101,7 @@ internal class DefaultTimeline( if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener } - if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { - handleInitialLoad() - } else { - handleUpdates(changeSet) - } + handleUpdates(changeSet) } private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> @@ -179,8 +163,9 @@ internal class DefaultTimeline( nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll() filteredEvents = nonFilteredEvents.where() .filterEventsWithSettings() - .findAllAsync() - .also { it.addChangeListener(eventsChangeListener) } + .findAll() + handleInitialLoad() + filteredEvents.addChangeListener(eventsChangeListener) eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) .findAllAsync() @@ -402,14 +387,14 @@ internal class DefaultTimeline( private fun getState(direction: Timeline.Direction): State { return when (direction) { - Timeline.Direction.FORWARDS -> forwardsState.get() + Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get() } } private fun updateState(direction: Timeline.Direction, update: (State) -> State) { val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsState + Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.BACKWARDS -> backwardsState } val currentValue = stateReference.get() @@ -497,9 +482,9 @@ internal class DefaultTimeline( return } val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -508,10 +493,10 @@ internal class DefaultTimeline( this.callback = object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { + TokenChunkEventPersistor.Result.SUCCESS -> { Timber.v("Success fetching $limit items $direction from pagination request") } - TokenChunkEventPersistor.Result.REACHED_END -> { + TokenChunkEventPersistor.Result.REACHED_END -> { postSnapshot() } TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> @@ -575,7 +560,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } From 7575cb286ed9056469a2962e3e80a29e992f0902 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 14:49:26 +0100 Subject: [PATCH 095/105] Show skip to bottom FAB while scrolling down (#752) --- CHANGES.md | 1 + .../im/vector/riotx/core/utils/Debouncer.kt | 16 +++++--- .../home/room/detail/RoomDetailFragment.kt | 38 +++++++++++++------ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bb132982d8..3087b7405c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ Improvements 🙌: - Improve devices list screen - Add settings for rageshake sensibility - Fix autocompletion issues and add support for rooms, groups, and emoji (#780) + - Show skip to bottom FAB while scrolling down (#752) Other changes: - Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt index b02e3c9366..2f5db57ccb 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -24,11 +24,9 @@ internal class Debouncer(private val handler: Handler) { private val runnables = HashMap() fun debounce(identifier: String, millis: Long, r: Runnable): Boolean { - if (runnables.containsKey(identifier)) { - // debounce - val old = runnables[identifier] - handler.removeCallbacks(old) - } + // debounce + cancel(identifier) + insertRunnable(identifier, r, millis) return true } @@ -37,6 +35,14 @@ internal class Debouncer(private val handler: Handler) { handler.removeCallbacksAndMessages(null) } + fun cancel(identifier: String) { + if (runnables.containsKey(identifier)) { + val old = runnables[identifier] + handler.removeCallbacks(old) + runnables.remove(identifier) + } + } + private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { val chained = Runnable { handler.post(r) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4414c48205..4e29aa2f89 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 @@ -474,21 +474,31 @@ class RoomDetailFragment @Inject constructor( it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) updateJumpToReadMarkerViewVisibility() - updateJumpToBottomViewVisibility() + maybeShowJumpToBottomViewVisibilityWithDelay() } timelineEventController.addModelBuildListener(modelBuildListener) recyclerView.adapter = timelineEventController.adapter recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + debouncer.cancel("jump_to_bottom_visibility") + + val scrollingToPast = dy < 0 + + if (scrollingToPast) { + jumpToBottomView.hide() + } else { + maybeShowJumpToBottomViewVisibility() + } + } + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { when (newState) { RecyclerView.SCROLL_STATE_IDLE -> { - updateJumpToBottomViewVisibility() + maybeShowJumpToBottomViewVisibilityWithDelay() } RecyclerView.SCROLL_STATE_DRAGGING, - RecyclerView.SCROLL_STATE_SETTLING -> { - jumpToBottomView.hide() - } + RecyclerView.SCROLL_STATE_SETTLING -> Unit } } }) @@ -547,17 +557,21 @@ class RoomDetailFragment @Inject constructor( } } - private fun updateJumpToBottomViewVisibility() { + private fun maybeShowJumpToBottomViewVisibilityWithDelay() { debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { - Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") - if (layoutManager.findFirstVisibleItemPosition() != 0) { - jumpToBottomView.show() - } else { - jumpToBottomView.hide() - } + maybeShowJumpToBottomViewVisibility() }) } + private fun maybeShowJumpToBottomViewVisibility() { + Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") + if (layoutManager.findFirstVisibleItemPosition() != 0) { + jumpToBottomView.show() + } else { + jumpToBottomView.hide() + } + } + private fun setupComposer() { autoCompleter.setup(composerLayout.composerEditText, this) From 501ac36040d5d1ecf960c6b5ad19ed6b8e8afc75 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 15:03:58 +0100 Subject: [PATCH 096/105] Reduce size of RoomDetailFragment --- .../im/vector/riotx/core/utils/Debouncer.kt | 2 +- .../JumpToBottomViewVisibilityManager.kt | 77 +++++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 49 +++--------- 3 files changed, 87 insertions(+), 41 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt index 2f5db57ccb..627d757574 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -19,7 +19,7 @@ package im.vector.riotx.core.utils import android.os.Handler -internal class Debouncer(private val handler: Handler) { +class Debouncer(private val handler: Handler) { private val runnables = HashMap() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt new file mode 100644 index 0000000000..4be5502678 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.FloatingActionButton +import im.vector.riotx.core.utils.Debouncer +import timber.log.Timber + +/** + * Show or hide the jumpToBottomView, depending on the scrolling and if the timeline is displaying the more recent event + * - When user scrolls up (i.e. going to the past): hide + * - When user scrolls down: show if not displaying last event + * - When user stops scrolling: show if not displaying last event + */ +class JumpToBottomViewVisibilityManager( + private val jumpToBottomView: FloatingActionButton, + private val debouncer: Debouncer, + recyclerView: RecyclerView, + private val layoutManager: LinearLayoutManager) { + + init { + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + debouncer.cancel("jump_to_bottom_visibility") + + val scrollingToPast = dy < 0 + + if (scrollingToPast) { + jumpToBottomView.hide() + } else { + maybeShowJumpToBottomViewVisibility() + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + when (newState) { + RecyclerView.SCROLL_STATE_IDLE -> { + maybeShowJumpToBottomViewVisibilityWithDelay() + } + RecyclerView.SCROLL_STATE_DRAGGING, + RecyclerView.SCROLL_STATE_SETTLING -> Unit + } + } + }) + } + + fun maybeShowJumpToBottomViewVisibilityWithDelay() { + debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { + maybeShowJumpToBottomViewVisibility() + }) + } + + private fun maybeShowJumpToBottomViewVisibility() { + Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") + if (layoutManager.findFirstVisibleItemPosition() != 0) { + jumpToBottomView.show() + } else { + jumpToBottomView.hide() + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4e29aa2f89..a956d0e2e9 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 @@ -181,6 +181,7 @@ class RoomDetailFragment @Inject constructor( private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var layoutManager: LinearLayoutManager + private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager private var modelBuildListener: OnModelBuildFinishedListener? = null private lateinit var attachmentsHelper: AttachmentsHelper @@ -312,6 +313,13 @@ class RoomDetailFragment @Inject constructor( } } } + + jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager( + jumpToBottomView, + debouncer, + recyclerView, + layoutManager + ) } private fun setupJumpToReadMarkerView() { @@ -474,35 +482,11 @@ class RoomDetailFragment @Inject constructor( it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) updateJumpToReadMarkerViewVisibility() - maybeShowJumpToBottomViewVisibilityWithDelay() + jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() } timelineEventController.addModelBuildListener(modelBuildListener) recyclerView.adapter = timelineEventController.adapter - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - debouncer.cancel("jump_to_bottom_visibility") - - val scrollingToPast = dy < 0 - - if (scrollingToPast) { - jumpToBottomView.hide() - } else { - maybeShowJumpToBottomViewVisibility() - } - } - - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - when (newState) { - RecyclerView.SCROLL_STATE_IDLE -> { - maybeShowJumpToBottomViewVisibilityWithDelay() - } - RecyclerView.SCROLL_STATE_DRAGGING, - RecyclerView.SCROLL_STATE_SETTLING -> Unit - } - } - }) - timelineEventController.callback = this if (vectorPreferences.swipeToReplyIsEnabled()) { @@ -557,21 +541,6 @@ class RoomDetailFragment @Inject constructor( } } - private fun maybeShowJumpToBottomViewVisibilityWithDelay() { - debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { - maybeShowJumpToBottomViewVisibility() - }) - } - - private fun maybeShowJumpToBottomViewVisibility() { - Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") - if (layoutManager.findFirstVisibleItemPosition() != 0) { - jumpToBottomView.show() - } else { - jumpToBottomView.hide() - } - } - private fun setupComposer() { autoCompleter.setup(composerLayout.composerEditText, this) From d662b4a9b4358b38e0ae97f78bb4492070e4d130 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 15:57:35 +0100 Subject: [PATCH 097/105] Exclude play-services-oss-licenses library from F-Droid build (#814) --- CHANGES.md | 1 + vector/build.gradle | 8 ++++--- .../fdroid/java/im/vector/riotx/FlavorCode.kt | 22 +++++++++++++++++ .../gplay/java/im/vector/riotx/FlavorCode.kt | 24 +++++++++++++++++++ .../VectorSettingsHelpAboutFragment.kt | 5 ++-- .../res/xml/vector_settings_help_about.xml | 4 +++- 6 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt create mode 100644 vector/src/gplay/java/im/vector/riotx/FlavorCode.kt diff --git a/CHANGES.md b/CHANGES.md index bb132982d8..49f2c33fdf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Improvements 🙌: Other changes: - Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800) + - Exclude play-services-oss-licenses library from F-Droid build (#814) Bugfix 🐛: - Fix crash when opening room creation screen from the room filtering screen diff --git a/vector/build.gradle b/vector/build.gradle index c8d474088f..513449db51 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -186,6 +186,7 @@ android { gplay { dimension "store" + resValue "bool", "isGplay", "true" buildConfigField "boolean", "ALLOW_FCM_USE", "true" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"" @@ -194,6 +195,7 @@ android { fdroid { dimension "store" + resValue "bool", "isGplay", "false" buildConfigField "boolean", "ALLOW_FCM_USE", "false" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" @@ -249,9 +251,6 @@ dependencies { implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" - // OSS License - implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' - // Log implementation 'com.jakewharton.timber:timber:4.7.1' @@ -343,6 +342,9 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } + // OSS License, gplay flavor only + gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' + implementation "androidx.emoji:emoji-appcompat:1.0.0" // TESTS diff --git a/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt b/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt new file mode 100644 index 0000000000..d6c9b9fd1d --- /dev/null +++ b/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx + +import android.content.Context + +// No op +fun openOssLicensesMenuActivity(context: Context) = Unit diff --git a/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt b/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt new file mode 100644 index 0000000000..7166cd5ed3 --- /dev/null +++ b/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx + +import android.content.Context +import android.content.Intent +import com.google.android.gms.oss.licenses.OssLicensesMenuActivity + +fun openOssLicensesMenuActivity(context: Context) = context.startActivity(Intent(context, OssLicensesMenuActivity::class.java)) + diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt index 6ce928c05d..6c10b8695d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt @@ -20,13 +20,13 @@ import android.content.Intent import android.net.Uri import android.provider.Settings import androidx.preference.Preference -import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import im.vector.matrix.android.api.Matrix import im.vector.riotx.R import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.core.utils.copyToClipboard import im.vector.riotx.core.utils.displayInWebView import im.vector.riotx.features.version.VersionProvider +import im.vector.riotx.openOssLicensesMenuActivity import javax.inject.Inject class VectorSettingsHelpAboutFragment @Inject constructor( @@ -107,10 +107,11 @@ class VectorSettingsHelpAboutFragment @Inject constructor( false } + // Note: preference is not visible on F-Droid build findPreference(VectorPreferences.SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY)!! .onPreferenceClickListener = Preference.OnPreferenceClickListener { // See https://developers.google.com/android/guides/opensource - startActivity(Intent(requireActivity(), OssLicensesMenuActivity::class.java)) + openOssLicensesMenuActivity(requireActivity()) false } } diff --git a/vector/src/main/res/xml/vector_settings_help_about.xml b/vector/src/main/res/xml/vector_settings_help_about.xml index 4aec87c123..2720a19533 100644 --- a/vector/src/main/res/xml/vector_settings_help_about.xml +++ b/vector/src/main/res/xml/vector_settings_help_about.xml @@ -1,5 +1,6 @@ + android:title="@string/settings_other_third_party_notices" + app:isPreferenceVisible="@bool/isGplay" /> \ No newline at end of file From 17c40133837ba2811e75b6e14819b7256869949a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 17:58:26 +0100 Subject: [PATCH 098/105] Developer mode: Fail-fast (#745) --- CHANGES.md | 2 +- .../java/im/vector/riotx/VectorApplication.kt | 5 ++-- .../main/java/im/vector/riotx/core/rx/Rx.kt | 25 ++++++++++++------- .../features/settings/VectorPreferences.kt | 5 ++++ vector/src/main/res/values/strings_riotX.xml | 3 +++ .../xml/vector_settings_advanced_settings.xml | 7 ++++++ 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bb132982d8..bf585df8e2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Improvements 🙌: - The initial sync is now handled by a foreground service - Render aliases and canonical alias change in the timeline - Fix autocompletion issues and add support for rooms and groups - - Introduce developer mode in the settings (#796) + - Introduce developer mode in the settings (#745, #796) - Improve devices list screen - Add settings for rageshake sensibility - Fix autocompletion issues and add support for rooms, groups, and emoji (#780) diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index 3f55e8f5e6..c76af027ba 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -42,7 +42,7 @@ import im.vector.riotx.core.di.DaggerVectorComponent import im.vector.riotx.core.di.HasVectorInjector import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.extensions.configureAndStart -import im.vector.riotx.core.rx.setupRxPlugin +import im.vector.riotx.core.rx.RxConfig import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.notifications.NotificationDrawerManager @@ -75,6 +75,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. @Inject lateinit var versionProvider: VersionProvider @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var appStateHandler: AppStateHandler + @Inject lateinit var rxConfig: RxConfig lateinit var vectorComponent: VectorComponent private var fontThreadHandler: Handler? = null @@ -84,7 +85,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. vectorComponent = DaggerVectorComponent.factory().create(this) vectorComponent.inject(this) vectorUncaughtExceptionHandler.activate(this) - setupRxPlugin() + rxConfig.setupRxPlugin() if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) diff --git a/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt b/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt index 89de9030dc..d8828eb1b8 100644 --- a/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt +++ b/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt @@ -17,19 +17,26 @@ package im.vector.riotx.core.rx import im.vector.riotx.BuildConfig +import im.vector.riotx.features.settings.VectorPreferences import io.reactivex.plugins.RxJavaPlugins import timber.log.Timber +import javax.inject.Inject -/** - * Make sure unhandled Rx error does not crash the app in production - */ -fun setupRxPlugin() { - RxJavaPlugins.setErrorHandler { throwable -> - Timber.e(throwable, "RxError") +class RxConfig @Inject constructor( + private val vectorPreferences: VectorPreferences +) { - // Avoid crash in production - if (BuildConfig.DEBUG) { - throw throwable + /** + * Make sure unhandled Rx error does not crash the app in production + */ + fun setupRxPlugin() { + RxJavaPlugins.setErrorHandler { throwable -> + Timber.e(throwable, "RxError") + + // Avoid crash in production + if (BuildConfig.DEBUG || vectorPreferences.failFast()) { + throw throwable + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 0ec67789fe..72f8cf01dd 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -151,6 +151,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY" private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" + private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY" // analytics const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" @@ -266,6 +267,10 @@ class VectorPreferences @Inject constructor(private val context: Context) { return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false) } + fun failFast(): Boolean { + return developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false) + } + /** * Tells if we have already asked the user to disable battery optimisations on android >= M devices. * diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 3e8485ebcc..d46231cff4 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -20,4 +20,7 @@ Showing only the first results, type more letters… + Fail-fast + RiotX may crash more often when an unexpected error occurs + diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml index 11ca97870d..131b43c8d5 100644 --- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -23,6 +23,13 @@ android:summary="@string/labs_allow_extended_logging_summary" android:title="@string/labs_allow_extended_logging" /> + + From 2b8ecae8e344e2a7753f30f0318d40a3f59daf96 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 18:05:26 +0100 Subject: [PATCH 099/105] Fix CI --- vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt | 2 +- vector/src/gplay/java/im/vector/riotx/FlavorCode.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt b/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt index d6c9b9fd1d..de1ee2290b 100644 --- a/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt +++ b/vector/src/fdroid/java/im/vector/riotx/FlavorCode.kt @@ -19,4 +19,4 @@ package im.vector.riotx import android.content.Context // No op -fun openOssLicensesMenuActivity(context: Context) = Unit +fun openOssLicensesMenuActivity(@Suppress("UNUSED_PARAMETER") context: Context) = Unit diff --git a/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt b/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt index 7166cd5ed3..109e9bc978 100644 --- a/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt +++ b/vector/src/gplay/java/im/vector/riotx/FlavorCode.kt @@ -21,4 +21,3 @@ import android.content.Intent import com.google.android.gms.oss.licenses.OssLicensesMenuActivity fun openOssLicensesMenuActivity(context: Context) = context.startActivity(Intent(context, OssLicensesMenuActivity::class.java)) - From 29f152f349a6ed3b4404d586372dd2fb3b06fde2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 18:21:01 +0100 Subject: [PATCH 100/105] Fix CI --- .../src/main/java/im/vector/riotx/core/rx/{Rx.kt => RxConfig.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vector/src/main/java/im/vector/riotx/core/rx/{Rx.kt => RxConfig.kt} (100%) diff --git a/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt b/vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt similarity index 100% rename from vector/src/main/java/im/vector/riotx/core/rx/Rx.kt rename to vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt From 80324906062253540d8f83b44fcff50b73d7ef20 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 18:58:51 +0100 Subject: [PATCH 101/105] Remove CountDownLatch (inspired from #419) --- .../internal/database/RealmQueryLatch.kt | 55 ++++++++----------- .../session/room/create/CreateRoomTask.kt | 14 ++--- .../room/membership/joining/JoinRoomTask.kt | 17 +++--- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt index 2d386eac15..98544d46f7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt @@ -16,43 +16,36 @@ package im.vector.matrix.android.internal.database -import im.vector.matrix.android.internal.util.createBackgroundHandler import io.realm.* -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.* -class RealmQueryLatch(private val realmConfiguration: RealmConfiguration, - private val realmQueryBuilder: (Realm) -> RealmQuery) { +internal suspend fun awaitNotEmptyResult(realmConfiguration: RealmConfiguration, + timeoutMillis: Long, + builder: (Realm) -> RealmQuery) { + withTimeout(timeoutMillis) { + // Confine Realm interaction to a single thread with Looper. + withContext(Dispatchers.Main) { + val latch = CompletableDeferred() - private companion object { - val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH") - } + Realm.getInstance(realmConfiguration).use { realm -> + val result = builder(realm).findAllAsync() - @Throws(InterruptedException::class) - fun await(timeout: Long, timeUnit: TimeUnit) { - val realmRef = AtomicReference() - val latch = CountDownLatch(1) - QUERY_LATCH_HANDLER.post { - val realm = Realm.getInstance(realmConfiguration) - realmRef.set(realm) - val result = realmQueryBuilder(realm).findAllAsync() - result.addChangeListener(object : RealmChangeListener> { - override fun onChange(t: RealmResults) { - if (t.isNotEmpty()) { - result.removeChangeListener(this) - latch.countDown() + val listener = object : RealmChangeListener> { + override fun onChange(it: RealmResults) { + if (it.isNotEmpty()) { + result.removeChangeListener(this) + latch.complete(Unit) + } } } - }) - } - try { - latch.await(timeout, timeUnit) - } catch (exception: InterruptedException) { - throw exception - } finally { - QUERY_LATCH_HANDLER.post { - realmRef.getAndSet(null).close() + + result.addChangeListener(listener) + try { + latch.await() + } catch (e: CancellationException) { + result.removeChangeListener(listener) + throw e + } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt index 9af8434b7c..970a1fed7e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt @@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse -import im.vector.matrix.android.internal.database.RealmQueryLatch +import im.vector.matrix.android.internal.database.awaitNotEmptyResult import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -34,6 +34,7 @@ import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAcco import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -53,13 +54,12 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro } val roomId = createRoomResponse.roomId!! // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) - val rql = RealmQueryLatch(realmConfiguration) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) - } try { - rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) - } catch (exception: Exception) { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + } catch (exception: TimeoutCancellationException) { throw CreateRoomFailure.CreatedWithTimeout } if (params.isDirect()) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt index 7304c09d57..fbede72520 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.session.room.membership.joining import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure -import im.vector.matrix.android.internal.database.RealmQueryLatch +import im.vector.matrix.android.internal.database.awaitNotEmptyResult import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.di.SessionDatabase @@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.task.Task import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -46,18 +47,16 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room executeRequest { apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason)) } - val roomId = params.roomId // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) - val rql = RealmQueryLatch(realmConfiguration) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) - } try { - rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) - } catch (exception: Exception) { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, params.roomId) + } + } catch (exception: TimeoutCancellationException) { throw JoinRoomFailure.JoinedWithTimeout } - setReadMarkers(roomId) + setReadMarkers(params.roomId) } private suspend fun setReadMarkers(roomId: String) { From 383605274c3b92969d1a1bdc436013d37417173a Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jan 2020 22:17:32 +0100 Subject: [PATCH 102/105] Introduce a very simple query langage and refact autocomplete --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 12 +- .../java/im/vector/matrix/rx/RxSession.kt | 7 +- .../android/api/query/QueryStringValue.kt | 35 ++++ .../android/api/session/group/GroupService.kt | 4 +- .../session/group/GroupSummaryQueryParams.kt | 44 +++++ .../session/room/RoomSummaryQueryParams.kt | 36 ++-- .../session/room/members/MembershipService.kt | 12 +- .../room/members/RoomMemberQueryParams.kt | 44 +++++ .../api/session/room/model/Membership.kt | 2 +- .../internal/query/QueryEnumListProcessor.kt | 22 ++- .../query/QueryStringValueProcessor.kt | 47 ++++++ .../session/group/DefaultGroupService.kt | 17 +- .../session/room/DefaultRoomService.kt | 19 +-- .../membership/DefaultMembershipService.kt | 26 ++- .../room/timeline/ClearUnlinkedEventsTask.kt | 1 - .../java/im/vector/riotx/AppStateHandler.kt | 5 +- .../emoji/AutocompleteEmojiController.kt | 8 +- .../group/AutocompleteGroupPresenter.kt | 29 ++-- .../member/AutocompleteMemberPresenter.kt | 37 +++- .../room/AutocompleteRoomController.kt | 4 +- .../room/AutocompleteRoomPresenter.kt | 30 ++-- .../features/home/group/GroupListViewModel.kt | 10 +- .../home/room/detail/AutoCompleter.kt | 48 +++--- .../home/room/detail/RoomDetailFragment.kt | 127 ++++++-------- .../detail/composer/TextComposerViewModel.kt | 158 ------------------ .../detail/composer/TextComposerViewState.kt | 34 ---- .../reactions/data/EmojiDataSource.kt | 3 +- .../roomdirectory/RoomDirectoryViewModel.kt | 13 +- .../roompreview/RoomPreviewViewModel.kt | 7 +- .../features/settings/VectorPreferences.kt | 4 +- .../features/share/IncomingShareViewModel.kt | 5 +- 31 files changed, 417 insertions(+), 433 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt rename vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt => matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt (52%) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt 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 48ff66720f..bbf0e76823 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -17,7 +17,11 @@ package im.vector.matrix.rx import im.vector.matrix.android.api.session.room.Room -import im.vector.matrix.android.api.session.room.model.* +import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams +import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary +import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -33,9 +37,9 @@ class RxRoom(private val room: Room) { .startWith(room.roomSummary().toOptional()) } - fun liveRoomMembers(memberships: List): Observable> { - return room.getRoomMembersLive(memberships).asObservable() - .startWith(room.getRoomMembers(memberships)) + fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable> { + return room.getRoomMembersLive(queryParams).asObservable() + .startWith(room.getRoomMembers(queryParams)) } fun liveAnnotationSummary(eventId: String): 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 c502e4d564..084f497de5 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -18,6 +18,7 @@ package im.vector.matrix.rx import androidx.paging.PagedList import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams @@ -36,9 +37,9 @@ class RxSession(private val session: Session) { .startWith(session.getRoomSummaries(queryParams)) } - fun liveGroupSummaries(): Observable> { - return session.getGroupSummariesLive().asObservable() - .startWith(session.getGroupSummaries()) + fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable> { + return session.getGroupSummariesLive(queryParams).asObservable() + .startWith(session.getGroupSummaries(queryParams)) } fun liveBreadcrumbs(): Observable> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt new file mode 100644 index 0000000000..5d3e76f1d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.query + +/** + * Basic query language. All these cases are mutually exclusive. + */ +sealed class QueryStringValue { + object NoCondition : QueryStringValue() + object IsNull : QueryStringValue() + object IsNotNull : QueryStringValue() + object IsEmpty : QueryStringValue() + object IsNotEmpty : QueryStringValue() + data class Equals(val string: String, val case: Case) : QueryStringValue() + data class Contains(val string: String, val case: Case) : QueryStringValue() + + enum class Case { + SENSITIVE, + INSENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt index b01bfa34ab..76bac1ae1a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt @@ -42,11 +42,11 @@ interface GroupService { * Get a list of group summaries.This list is a snapshot of the data. * @return the list of [GroupSummary] */ - fun getGroupSummaries(): List + fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List /** * Get a live list of group summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of [GroupSummary] */ - fun getGroupSummariesLive(): LiveData> + fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt new file mode 100644 index 0000000000..702b8c2523 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.group + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.room.model.Membership + +fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams { + return GroupSummaryQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter group summaries + */ +data class GroupSummaryQueryParams( + val displayName: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List = Membership.all() + + fun build() = GroupSummaryQueryParams( + displayName = displayName, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt index 00a17f083d..6983bda225 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt @@ -16,23 +16,33 @@ package im.vector.matrix.android.api.session.room +import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.room.model.Membership +fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { + return RoomSummaryQueryParams.Builder().apply(init).build() +} + /** * This class can be used to filter room summaries to use with: * [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService] */ data class RoomSummaryQueryParams( - /** - * Set to true if you want only non null display name. True by default - */ - val filterDisplayName: Boolean = true, - /** - * Set to true if you want only non null canonical alias. False by default. - */ - val filterCanonicalAlias: Boolean = false, - /** - * Set the list of memberships you want to filter on. By default, all memberships. - */ - val memberships: List = Membership.all() -) + val displayName: QueryStringValue, + val canonicalAlias: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition + var memberships: List = Membership.all() + + fun build() = RoomSummaryQueryParams( + displayName = displayName, + canonicalAlias = canonicalAlias, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index 281697816c..6c117d3be7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.api.session.room.members import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable @@ -41,20 +40,19 @@ interface MembershipService { */ fun getRoomMember(userId: String): RoomMember? - /** - * Return all the roomMembers of the room filtered by memberships - * @param memberships list of accepted memberships + * Return all the roomMembers of the room with params + * @param queryParams the params to query for * @return a roomMember list. */ - fun getRoomMembers(memberships: List): List + fun getRoomMembers(queryParams: RoomMemberQueryParams): List /** * Return all the roomMembers of the room filtered by memberships - * @param memberships list of accepted memberships + * @param queryParams the params to query for * @return a [LiveData] of roomMember list. */ - fun getRoomMembersLive(memberships: List): LiveData> + fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> fun getNumberOfJoinedMembers(): Int diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt new file mode 100644 index 0000000000..19003632ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.members + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.room.model.Membership + +fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}): RoomMemberQueryParams { + return RoomMemberQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter room members + */ +data class RoomMemberQueryParams( + val displayName: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List = Membership.all() + + fun build() = RoomMemberQueryParams( + displayName = displayName, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt index 5182d2099e..7c6a931373 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt @@ -49,7 +49,7 @@ enum class Membership(val value: String) { return listOf(INVITE, JOIN) } - fun all(): List{ + fun all(): List { return values().asList() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt similarity index 52% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt index 0f5bf2a8c5..7cb5ca1047 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,20 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.composer +package im.vector.matrix.android.internal.query -import im.vector.riotx.core.platform.VectorViewModelAction +import io.realm.RealmObject +import io.realm.RealmQuery -sealed class TextComposerAction : VectorViewModelAction { - data class QueryUsers(val query: CharSequence?) : TextComposerAction() - data class QueryRooms(val query: CharSequence?) : TextComposerAction() - data class QueryGroups(val query: CharSequence?) : TextComposerAction() +fun > RealmQuery.process(field: String, enums: List>): RealmQuery { + val lastEnumValue = enums.lastOrNull() + this.beginGroup() + for (enumValue in enums) { + this.equalTo(field, enumValue.name) + if (enumValue != lastEnumValue) { + this.or() + } + } + this.endGroup() + return this } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt new file mode 100644 index 0000000000..d2418ee84f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.query + +import im.vector.matrix.android.api.query.QueryStringValue +import io.realm.Case +import io.realm.RealmObject +import io.realm.RealmQuery +import timber.log.Timber + +fun RealmQuery.process(field: String, queryStringValue: QueryStringValue): RealmQuery { + when (queryStringValue) { + is QueryStringValue.NoCondition -> Timber.v("No condition to process") + is QueryStringValue.IsNotNull -> this.isNotNull(field) + is QueryStringValue.IsNull -> this.isNull(field) + is QueryStringValue.IsEmpty -> this.isEmpty(field) + is QueryStringValue.IsNotEmpty -> this.isNotEmpty(field) + is QueryStringValue.Equals -> { + this.equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + } + is QueryStringValue.Contains -> { + this.contains(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + } + } + return this +} + +private fun QueryStringValue.Case.toRealmCase(): Case { + return when (this) { + QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE + QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt index 584b577443..baa8f5218d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt @@ -20,11 +20,13 @@ import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.group.Group import im.vector.matrix.android.api.session.group.GroupService +import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.util.fetchCopyMap import io.realm.Realm import io.realm.RealmQuery @@ -43,22 +45,23 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon ) } - override fun getGroupSummaries(): List { + override fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List { return monarchy.fetchAllMappedSync( - { groupSummariesQuery(it) }, + { groupSummariesQuery(it, groupSummaryQueryParams) }, { it.asDomain() } ) } - - override fun getGroupSummariesLive(): LiveData> { + override fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData> { return monarchy.findAllMappedWithChanges( - { groupSummariesQuery(it) }, + { groupSummariesQuery(it, groupSummaryQueryParams) }, { it.asDomain() } ) } - private fun groupSummariesQuery(realm: Realm): RealmQuery { - return GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) + private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery { + return GroupSummaryEntity.where(realm) + .process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + .process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 19c347ba8c..0cfc5aad3c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -33,6 +33,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.findByAlias import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask @@ -104,21 +105,9 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { val query = RoomSummaryEntity.where(realm) - if (queryParams.filterCanonicalAlias) { - query.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - } - if (queryParams.filterCanonicalAlias) { - query.isNotEmpty(RoomSummaryEntityFields.CANONICAL_ALIAS) - } - val lastMembership = queryParams.memberships.lastOrNull() - query.beginGroup() - for (membership in queryParams.memberships) { - query.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, membership.name) - if (membership != lastMembership) { - query.or() - } - } - query.endGroup() + query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) + query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) return query } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 2f8ce763b6..679f4a050b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -22,12 +22,14 @@ import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.members.MembershipService +import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.RoomMemberEntity import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields +import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask @@ -67,10 +69,10 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr return roomMemberEntity?.asDomain() } - override fun getRoomMembers(memberships: List): List { + override fun getRoomMembers(queryParams: RoomMemberQueryParams): List { return monarchy.fetchAllMappedSync( { - roomMembersQuery(it, memberships) + roomMembersQuery(it, queryParams) }, { it.asDomain() @@ -78,10 +80,10 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr ) } - override fun getRoomMembersLive(memberships: List): LiveData> { + override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> { return monarchy.findAllMappedWithChanges( { - roomMembersQuery(it, memberships) + roomMembersQuery(it, queryParams) }, { it.asDomain() @@ -89,18 +91,10 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr ) } - private fun roomMembersQuery(realm: Realm, memberships: List): RealmQuery { - val query = RoomMembers(realm, roomId).queryRoomMembersEvent() - val lastMembership = memberships.lastOrNull() - query.beginGroup() - for (membership in memberships) { - query.equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, membership.name) - if (membership != lastMembership) { - query.or() - } - } - query.endGroup() - return query + private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery { + return RoomMembers(realm, roomId).queryRoomMembersEvent() + .process(RoomMemberEntityFields.MEMBERSHIP_STR, queryParams.memberships) + .process(RoomMemberEntityFields.DISPLAY_NAME, queryParams.displayName) } override fun getNumberOfJoinedMembers(): Int { 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 d92320abf9..b532d61914 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt @@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt index 78a880854e..936253de28 100644 --- a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt @@ -21,8 +21,8 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import arrow.core.Option import im.vector.matrix.android.api.session.group.model.GroupSummary -import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.matrix.rx.rx import im.vector.riotx.features.home.HomeRoomListDataSource import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID @@ -66,7 +66,8 @@ class AppStateHandler @Inject constructor( sessionDataSource.observe() .observeOn(AndroidSchedulers.mainThread()) .switchMap { - it.orNull()?.rx()?.liveRoomSummaries(RoomSummaryQueryParams()) + val query = roomSummaryQueryParams {} + it.orNull()?.rx()?.liveRoomSummaries(query) ?: Observable.just(emptyList()) } .throttleLast(300, TimeUnit.MILLISECONDS), diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt index 010b362b68..6d498de2d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -37,10 +37,6 @@ class AutocompleteEmojiController @Inject constructor( } } - init { - fontProvider.addListener(fontProviderListener) - } - var listener: AutocompleteClickListener? = null override fun buildModels(data: List?) { @@ -71,6 +67,10 @@ class AutocompleteEmojiController @Inject constructor( } } + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + fontProvider.addListener(fontProviderListener) + } + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { super.onDetachedFromRecyclerView(recyclerView) fontProvider.removeListener(fontProviderListener) diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt index 822ce451e7..8a726a81b1 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt @@ -19,18 +19,19 @@ package im.vector.riotx.features.autocomplete.group import android.content.Context import androidx.recyclerview.widget.RecyclerView import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Success import com.otaliastudios.autocomplete.RecyclerViewPresenter +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.group.groupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.features.autocomplete.AutocompleteClickListener import javax.inject.Inject class AutocompleteGroupPresenter @Inject constructor(context: Context, - private val controller: AutocompleteGroupController + private val controller: AutocompleteGroupController, + private val session: Session ) : RecyclerViewPresenter(context), AutocompleteClickListener { - var callback: Callback? = null - init { controller.listener = this } @@ -46,16 +47,20 @@ class AutocompleteGroupPresenter @Inject constructor(context: Context, } override fun onQuery(query: CharSequence?) { - callback?.onQueryGroups(query) + val queryParams = groupSummaryQueryParams { + displayName = if (query.isNullOrBlank()) { + QueryStringValue.IsNotEmpty + } else { + QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE) + } + } + val groups = session.getGroupSummaries(queryParams) + .asSequence() + .sortedBy { it.displayName } + controller.setData(groups.toList()) } fun render(groups: Async>) { - if (groups is Success) { - controller.setData(groups()) - } - } - - interface Callback { - fun onQueryGroups(query: CharSequence?) + controller.setData(groups()) } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt index b39d1d9b44..5bf573ecb1 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -21,20 +21,32 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.mvrx.Async import com.airbnb.mvrx.Success import com.otaliastudios.autocomplete.RecyclerViewPresenter +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.riotx.features.autocomplete.AutocompleteClickListener -import javax.inject.Inject -class AutocompleteMemberPresenter @Inject constructor(context: Context, - private val controller: AutocompleteMemberController +class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, + @Assisted val roomId: String, + private val session: Session, + private val controller: AutocompleteMemberController ) : RecyclerViewPresenter(context), AutocompleteClickListener { - var callback: Callback? = null + private val room = session.getRoom(roomId)!! init { controller.listener = this } + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): AutocompleteMemberPresenter + } + override fun instantiateAdapter(): RecyclerView.Adapter<*> { // Also remove animation recyclerView?.itemAnimator = null @@ -46,7 +58,18 @@ class AutocompleteMemberPresenter @Inject constructor(context: Context, } override fun onQuery(query: CharSequence?) { - callback?.onQueryMembers(query) + val queryParams = roomMemberQueryParams { + displayName = if (query.isNullOrBlank()) { + QueryStringValue.IsNotEmpty + } else { + QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE) + } + memberships = listOf(Membership.JOIN) + } + val members = room.getRoomMembers(queryParams) + .asSequence() + .sortedBy { it.displayName } + controller.setData(members.toList()) } fun render(members: Async>) { @@ -54,8 +77,4 @@ class AutocompleteMemberPresenter @Inject constructor(context: Context, controller.setData(members()) } } - - interface Callback { - fun onQueryMembers(query: CharSequence?) - } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt index 51285b02b7..aae95502d9 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt @@ -24,12 +24,10 @@ import im.vector.riotx.features.autocomplete.autocompleteMatrixItem import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject -class AutocompleteRoomController @Inject constructor() : TypedEpoxyController>() { +class AutocompleteRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer) : TypedEpoxyController>() { var listener: AutocompleteClickListener? = null - @Inject lateinit var avatarRenderer: AvatarRenderer - override fun buildModels(data: List?) { if (data.isNullOrEmpty()) { return diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt index 53fed7f859..17787a22ef 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt @@ -18,19 +18,19 @@ package im.vector.riotx.features.autocomplete.room import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Success import com.otaliastudios.autocomplete.RecyclerViewPresenter +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.riotx.features.autocomplete.AutocompleteClickListener import javax.inject.Inject class AutocompleteRoomPresenter @Inject constructor(context: Context, - private val controller: AutocompleteRoomController + private val controller: AutocompleteRoomController, + private val session: Session ) : RecyclerViewPresenter(context), AutocompleteClickListener { - var callback: Callback? = null - init { controller.listener = this } @@ -46,16 +46,16 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context, } override fun onQuery(query: CharSequence?) { - callback?.onQueryRooms(query) - } - - fun render(rooms: Async>) { - if (rooms is Success) { - controller.setData(rooms()) + val queryParams = roomSummaryQueryParams { + canonicalAlias = if (query.isNullOrBlank()) { + QueryStringValue.IsNotNull + } else { + QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE) + } } - } - - interface Callback { - fun onQueryRooms(query: CharSequence?) + val rooms = session.getRoomSummaries(queryParams) + .asSequence() + .sortedBy { it.displayName } + controller.setData(rooms.toList()) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index 24318bc508..a00ee24b49 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -24,7 +24,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.group.groupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.rx.rx @@ -96,6 +98,10 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro } private fun observeGroupSummaries() { + val groupSummariesQueryParams = groupSummaryQueryParams { + memberships = listOf(Membership.JOIN) + displayName = QueryStringValue.IsNotEmpty + } Observable.combineLatest, List>( session .rx() @@ -109,9 +115,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro }, session .rx() - .liveGroupSummaries() - // Keep only joined groups. Group invitations will be managed later - .map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } }, + .liveGroupSummaries(groupSummariesQueryParams), BiFunction { allCommunityGroup, communityGroups -> listOf(allCommunityGroup) + communityGroups } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 1500fb4dfb..6ae937fed2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -24,6 +24,8 @@ import android.widget.EditText import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary @@ -40,20 +42,25 @@ import im.vector.riotx.features.autocomplete.member.AutocompleteMemberPresenter import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.themes.ThemeUtils -import javax.inject.Inject -class AutoCompleter @Inject constructor( +class AutoCompleter @AssistedInject constructor( + @Assisted val roomId: String, private val avatarRenderer: AvatarRenderer, private val commandAutocompletePolicy: CommandAutocompletePolicy, private val autocompleteCommandPresenter: AutocompleteCommandPresenter, - private val autocompleteMemberPresenter: AutocompleteMemberPresenter, + private val autocompleteMemberPresenter: AutocompleteMemberPresenter.Factory, private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val autocompleteGroupPresenter: AutocompleteGroupPresenter, private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter ) { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): AutoCompleter + } + private lateinit var editText: EditText fun enterSpecialMode() { @@ -68,22 +75,14 @@ class AutoCompleter @Inject constructor( GlideApp.with(editText) } - fun setup(editText: EditText, listener: AutoCompleterListener) { + fun setup(editText: EditText) { this.editText = editText - val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background)) - setupCommands(backgroundDrawable, editText) - setupUsers(backgroundDrawable, editText, listener) - setupRooms(backgroundDrawable, editText, listener) - setupGroups(backgroundDrawable, editText, listener) + setupMembers(backgroundDrawable, editText) + setupGroups(backgroundDrawable, editText) setupEmojis(backgroundDrawable, editText) - } - - fun render(state: TextComposerViewState) { - autocompleteMemberPresenter.render(state.asyncMembers) - autocompleteRoomPresenter.render(state.asyncRooms) - autocompleteGroupPresenter.render(state.asyncGroups) + setupRooms(backgroundDrawable, editText) } private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) { @@ -107,11 +106,11 @@ class AutoCompleter @Inject constructor( .build() } - private fun setupUsers(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteMemberPresenter.Callback) { - autocompleteMemberPresenter.callback = listener + private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) { + val membersPresenter = autocompleteMemberPresenter.create(roomId) Autocomplete.on(editText) .with(CharPolicy('@', true)) - .with(autocompleteMemberPresenter) + .with(membersPresenter) .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { @@ -126,8 +125,7 @@ class AutoCompleter @Inject constructor( .build() } - private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteRoomPresenter.Callback) { - autocompleteRoomPresenter.callback = listener + private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) { Autocomplete.on(editText) .with(CharPolicy('#', true)) .with(autocompleteRoomPresenter) @@ -145,8 +143,7 @@ class AutoCompleter @Inject constructor( .build() } - private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteGroupPresenter.Callback) { - autocompleteGroupPresenter.callback = listener + private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) { Autocomplete.on(editText) .with(CharPolicy('+', true)) .with(autocompleteGroupPresenter) @@ -226,11 +223,6 @@ class AutoCompleter @Inject constructor( editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } - interface AutoCompleterListener : - AutocompleteMemberPresenter.Callback, - AutocompleteRoomPresenter.Callback, - AutocompleteGroupPresenter.Callback - companion object { private const val ELEVATION = 6f } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 757507fc1e..6a13b22f82 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -78,10 +78,7 @@ import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.getColorFromUserId -import im.vector.riotx.features.home.room.detail.composer.TextComposerAction import im.vector.riotx.features.home.room.detail.composer.TextComposerView -import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel -import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedAction @@ -127,17 +124,15 @@ class RoomDetailFragment @Inject constructor( private val session: Session, private val avatarRenderer: AvatarRenderer, private val timelineEventController: TimelineEventController, - private val autoCompleter: AutoCompleter, + autoCompleterFactory: AutoCompleter.Factory, private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, val roomDetailViewModelFactory: RoomDetailViewModel.Factory, - val textComposerViewModelFactory: TextComposerViewModel.Factory, private val eventHtmlRenderer: EventHtmlRenderer, private val vectorPreferences: VectorPreferences ) : VectorBaseFragment(), TimelineEventController.Callback, - AutoCompleter.AutoCompleterListener, VectorInviteView.Callback, JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, @@ -167,9 +162,10 @@ class RoomDetailFragment @Inject constructor( GlideApp.with(this) } + private val autoCompleter: AutoCompleter by lazy { + autoCompleterFactory.create(roomDetailArgs.roomId) + } private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() - private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() - private val debouncer = Debouncer(createUIHandler()) private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback @@ -206,7 +202,6 @@ class RoomDetailFragment @Inject constructor( setupJumpToBottomView() roomDetailViewModel.subscribe { renderState(it) } - textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) } roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair -> @@ -250,9 +245,9 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode -> when (mode) { is SendMode.REGULAR -> renderRegularMode(mode.text) - is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) - is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) - is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) + is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) + is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) + is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) } } @@ -279,9 +274,9 @@ class RoomDetailFragment @Inject constructor( super.onActivityCreated(savedInstanceState) if (savedInstanceState == null) { when (val sharedData = roomDetailArgs.sharedData) { - is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false)) + is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false)) is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData)) - null -> Timber.v("No share data to process") + null -> Timber.v("No share data to process") } } } @@ -485,7 +480,7 @@ class RoomDetailFragment @Inject constructor( recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { when (newState) { - RecyclerView.SCROLL_STATE_IDLE -> { + RecyclerView.SCROLL_STATE_IDLE -> { updateJumpToBottomViewVisibility() } RecyclerView.SCROLL_STATE_DRAGGING, @@ -512,7 +507,7 @@ class RoomDetailFragment @Inject constructor( is MessageTextItem -> { return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED } - else -> false + else -> false } } } @@ -527,9 +522,9 @@ class RoomDetailFragment @Inject constructor( withState(roomDetailViewModel) { val showJumpToUnreadBanner = when (it.unreadState) { UnreadState.Unknown, - UnreadState.HasNoUnread -> false + UnreadState.HasNoUnread -> false is UnreadState.ReadMarkerNotLoaded -> true - is UnreadState.HasUnread -> { + is UnreadState.HasUnread -> { if (it.canShowJumpToReadMarker) { val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() @@ -560,7 +555,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupComposer() { - autoCompleter.setup(composerLayout.composerEditText, this) + autoCompleter.setup(composerLayout.composerEditText) composerLayout.callback = object : TextComposerView.Callback { override fun onAddAttachment() { if (!::attachmentTypeSelector.isInitialized) { @@ -656,10 +651,6 @@ class RoomDetailFragment @Inject constructor( } } - private fun renderTextComposerState(state: TextComposerViewState) { - autoCompleter.render(state) - } - private fun renderTombstoneEventHandling(async: Async) { when (async) { is Loading -> { @@ -672,7 +663,7 @@ class RoomDetailFragment @Inject constructor( navigator.openRoom(vectorBaseActivity, async()) vectorBaseActivity.finish() } - is Fail -> { + is Fail -> { vectorBaseActivity.hideWaitingView() vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) } @@ -681,23 +672,23 @@ class RoomDetailFragment @Inject constructor( private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { - is SendMessageResult.MessageSent -> { + is SendMessageResult.MessageSent -> { updateComposerText("") } - is SendMessageResult.SlashCommandHandled -> { + is SendMessageResult.SlashCommandHandled -> { sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } updateComposerText("") } - is SendMessageResult.SlashCommandError -> { + is SendMessageResult.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) } - is SendMessageResult.SlashCommandUnknown -> { + is SendMessageResult.SlashCommandUnknown -> { displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } - is SendMessageResult.SlashCommandResultOk -> { + is SendMessageResult.SlashCommandResultOk -> { updateComposerText("") } - is SendMessageResult.SlashCommandResultError -> { + is SendMessageResult.SlashCommandResultError -> { displayCommandError(sendMessageResult.throwable.localizedMessage) } is SendMessageResult.SlashCommandNotImplemented -> { @@ -735,7 +726,7 @@ class RoomDetailFragment @Inject constructor( private fun displayRoomDetailActionResult(result: Async) { when (result) { - is Fail -> { + is Fail -> { AlertDialog.Builder(requireActivity()) .setTitle(R.string.dialog_title_error) .setMessage(errorFormatter.toHumanReadable(result.error)) @@ -746,7 +737,7 @@ class RoomDetailFragment @Inject constructor( when (val data = result.invoke()) { is RoomDetailAction.ReportContent -> { when { - data.spam -> { + data.spam -> { AlertDialog.Builder(requireActivity()) .setTitle(R.string.content_reported_as_spam_title) .setMessage(R.string.content_reported_as_spam_content) @@ -768,7 +759,7 @@ class RoomDetailFragment @Inject constructor( .show() .withColoredButton(DialogInterface.BUTTON_NEGATIVE) } - else -> { + else -> { AlertDialog.Builder(requireActivity()) .setTitle(R.string.content_reported_title) .setMessage(R.string.content_reported_content) @@ -881,14 +872,14 @@ class RoomDetailFragment @Inject constructor( override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (allGranted(grantResults)) { when (requestCode) { - PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> { + PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> { val action = roomDetailViewModel.pendingAction if (action != null) { roomDetailViewModel.pendingAction = null roomDetailViewModel.handle(action) } } - PERMISSION_REQUEST_CODE_INCOMING_URI -> { + PERMISSION_REQUEST_CODE_INCOMING_URI -> { val pendingUri = roomDetailViewModel.pendingUri if (pendingUri != null) { roomDetailViewModel.pendingUri = null @@ -984,43 +975,25 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } - // AutocompleteMemberPresenter.Callback - - override fun onQueryMembers(query: CharSequence?) { - textComposerViewModel.handle(TextComposerAction.QueryUsers(query)) - } - - // AutocompleteRoomPresenter.Callback - - override fun onQueryRooms(query: CharSequence?) { - textComposerViewModel.handle(TextComposerAction.QueryRooms(query)) - } - - // AutocompleteGroupPresenter.Callback - - override fun onQueryGroups(query: CharSequence?) { - textComposerViewModel.handle(TextComposerAction.QueryGroups(query)) - } - private fun handleActions(action: EventSharedAction) { when (action) { - is EventSharedAction.AddReaction -> { + is EventSharedAction.AddReaction -> { startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) } - is EventSharedAction.ViewReactions -> { + is EventSharedAction.ViewReactions -> { ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } - is EventSharedAction.Copy -> { + is EventSharedAction.Copy -> { // I need info about the current selected message :/ copyToClipboard(requireContext(), action.content, false) val msg = requireContext().getString(R.string.copied_to_clipboard) showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) } - is EventSharedAction.Delete -> { + is EventSharedAction.Delete -> { roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) } - is EventSharedAction.Share -> { + is EventSharedAction.Share -> { // TODO current data communication is too limited // Need to now the media type // TODO bad, just POC @@ -1048,10 +1021,10 @@ class RoomDetailFragment @Inject constructor( } ) } - is EventSharedAction.ViewEditHistory -> { + is EventSharedAction.ViewEditHistory -> { onEditedDecorationClicked(action.messageInformationData) } - is EventSharedAction.ViewSource -> { + is EventSharedAction.ViewSource -> { val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) view.findViewById(R.id.event_content_text_view)?.let { it.text = action.content @@ -1062,7 +1035,7 @@ class RoomDetailFragment @Inject constructor( .setPositiveButton(R.string.ok, null) .show() } - is EventSharedAction.ViewDecryptedSource -> { + is EventSharedAction.ViewDecryptedSource -> { val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) view.findViewById(R.id.event_content_text_view)?.let { it.text = action.content @@ -1073,31 +1046,31 @@ class RoomDetailFragment @Inject constructor( .setPositiveButton(R.string.ok, null) .show() } - is EventSharedAction.QuickReact -> { + is EventSharedAction.QuickReact -> { // eventId,ClickedOn,Add roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } - is EventSharedAction.Edit -> { + is EventSharedAction.Edit -> { roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, composerLayout.text.toString())) } - is EventSharedAction.Quote -> { + is EventSharedAction.Quote -> { roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, composerLayout.text.toString())) } - is EventSharedAction.Reply -> { + is EventSharedAction.Reply -> { roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, composerLayout.text.toString())) } - is EventSharedAction.CopyPermalink -> { + is EventSharedAction.CopyPermalink -> { val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) } - is EventSharedAction.Resend -> { + is EventSharedAction.Resend -> { roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId)) } - is EventSharedAction.Remove -> { + is EventSharedAction.Remove -> { roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId)) } - is EventSharedAction.ReportContentSpam -> { + is EventSharedAction.ReportContentSpam -> { roomDetailViewModel.handle(RoomDetailAction.ReportContent( action.eventId, action.senderId, "This message is spam", spam = true)) } @@ -1105,19 +1078,19 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.ReportContent( action.eventId, action.senderId, "This message is inappropriate", inappropriate = true)) } - is EventSharedAction.ReportContentCustom -> { + is EventSharedAction.ReportContentCustom -> { promptReasonToReportContent(action) } - is EventSharedAction.IgnoreUser -> { + is EventSharedAction.IgnoreUser -> { roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId)) } - is EventSharedAction.OnUrlClicked -> { + is EventSharedAction.OnUrlClicked -> { onUrlClicked(action.url) } - is EventSharedAction.OnUrlLongClicked -> { + is EventSharedAction.OnUrlLongClicked -> { onUrlLongClicked(action.url) } - else -> { + else -> { Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } } @@ -1225,10 +1198,10 @@ class RoomDetailFragment @Inject constructor( private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera() - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile() + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera() + AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile() AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery() - AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio() + AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio() AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact() AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers") } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt deleted file mode 100644 index 37e01fcacf..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.home.room.detail.composer - -import arrow.core.Option -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.group.model.GroupSummary -import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams -import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.rx.rx -import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.features.home.room.detail.RoomDetailFragment -import io.reactivex.Observable -import io.reactivex.functions.BiFunction -import java.util.concurrent.TimeUnit - -typealias AutocompleteQuery = CharSequence - -class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState, - private val session: Session -) : VectorViewModel(initialState) { - - private val room = session.getRoom(initialState.roomId)!! - - private val usersQueryObservable = BehaviorRelay.create>() - private val roomsQueryObservable = BehaviorRelay.create>() - private val groupsQueryObservable = BehaviorRelay.create>() - - @AssistedInject.Factory - interface Factory { - fun create(initialState: TextComposerViewState): TextComposerViewModel - } - - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel? { - val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.textComposerViewModelFactory.create(state) - } - } - - init { - observeUsersQuery() - observeRoomsQuery() - observeGroupsQuery() - } - - override fun handle(action: TextComposerAction) { - when (action) { - is TextComposerAction.QueryUsers -> handleQueryUsers(action) - is TextComposerAction.QueryRooms -> handleQueryRooms(action) - is TextComposerAction.QueryGroups -> handleQueryGroups(action) - } - } - - private fun handleQueryUsers(action: TextComposerAction.QueryUsers) { - val query = Option.fromNullable(action.query) - usersQueryObservable.accept(query) - } - - private fun handleQueryRooms(action: TextComposerAction.QueryRooms) { - val query = Option.fromNullable(action.query) - roomsQueryObservable.accept(query) - } - - private fun handleQueryGroups(action: TextComposerAction.QueryGroups) { - val query = Option.fromNullable(action.query) - groupsQueryObservable.accept(query) - } - - private fun observeUsersQuery() { - Observable.combineLatest, Option, List>( - room.rx().liveRoomMembers(Membership.activeMemberships()), - usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), - BiFunction { roomMembers, query -> - val filter = query.orNull() - if (filter.isNullOrBlank()) { - roomMembers - } else { - roomMembers.filter { - it.displayName?.contains(filter, ignoreCase = true) ?: false - } - } - .sortedBy { it.displayName } - } - ).execute { async -> - copy( - asyncMembers = async - ) - } - } - - private fun observeRoomsQuery() { - Observable.combineLatest, Option, List>( - session.rx().liveRoomSummaries(RoomSummaryQueryParams(filterCanonicalAlias = true)), - roomsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), - BiFunction { roomSummaries, query -> - val filter = query.orNull() ?: "" - // Keep only room with a canonical alias - roomSummaries - .filter { - it.canonicalAlias?.contains(filter, ignoreCase = true) == true - } - .sortedBy { it.displayName } - } - ).execute { async -> - copy( - asyncRooms = async - ) - } - } - - private fun observeGroupsQuery() { - Observable.combineLatest, Option, List>( - session.rx().liveGroupSummaries(), - groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), - BiFunction { groupSummaries, query -> - val filter = query.orNull() - if (filter.isNullOrBlank()) { - groupSummaries - } else { - groupSummaries - .filter { - it.groupId.contains(filter, ignoreCase = true) - } - } - .sortedBy { it.displayName } - } - ).execute { async -> - copy( - asyncGroups = async - ) - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt deleted file mode 100644 index f2a1f07c57..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.home.room.detail.composer - -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.api.session.group.model.GroupSummary -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.riotx.features.home.room.detail.RoomDetailArgs - -data class TextComposerViewState(val roomId: String, - val asyncMembers: Async> = Uninitialized, - val asyncRooms: Async> = Uninitialized, - val asyncGroups: Async> = Uninitialized -) : MvRxState { - - constructor(args: RoomDetailArgs) : this(roomId = args.roomId) -} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 2917dce68a..1d7338e2a4 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.reactions.data import android.content.res.Resources import com.squareup.moshi.Moshi import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenScope import javax.inject.Inject import javax.inject.Singleton @@ -42,6 +41,7 @@ class EmojiDataSource @Inject constructor( // First add emojis with name matching query, sorted by name return (rawData.emojis.values + .asSequence() .filter { emojiItem -> emojiItem.name.contains(query, true) } @@ -56,6 +56,7 @@ class EmojiDataSource @Inject constructor( .sortedBy { it.name }) // and ensure they will not be present twice .distinct() + .toList() } fun getQuickReactions(): List { diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index cc57775028..c4a91a520a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -24,12 +24,12 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsFilter import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData +import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.rx.rx import im.vector.riotx.core.extensions.postLiveEvent @@ -80,9 +80,12 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: } private fun observeJoinedRooms() { + val queryParams = roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + } session .rx() - .liveRoomSummaries(RoomSummaryQueryParams(memberships = listOf(Membership.JOIN))) + .liveRoomSummaries(queryParams) .subscribe { list -> val joinedRoomIds = list ?.map { it.roomId } @@ -105,9 +108,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: RoomDirectoryAction) { when (action) { is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action) - is RoomDirectoryAction.FilterWith -> filterWith(action) - RoomDirectoryAction.LoadMore -> loadMore() - is RoomDirectoryAction.JoinRoom -> joinRoom(action) + is RoomDirectoryAction.FilterWith -> filterWith(action) + RoomDirectoryAction.LoadMore -> loadMore() + is RoomDirectoryAction.JoinRoom -> joinRoom(action) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index 005a930fdc..3de5cb4334 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -23,8 +23,8 @@ 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.session.Session -import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.roomdirectory.JoinState @@ -54,9 +54,12 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R } private fun observeJoinedRooms() { + val queryParams = roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + } session .rx() - .liveRoomSummaries(RoomSummaryQueryParams(memberships = listOf(Membership.JOIN))) + .liveRoomSummaries(queryParams) .subscribe { list -> withState { state -> val isRoomJoined = list diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 0ec67789fe..774e3741e6 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -398,7 +398,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { fun getNotificationRingTone(): Uri? { val url = defaultPrefs.getString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, null) - // the user selects "None" + // the user selects "NoCondition" if (url == "") { return null } @@ -425,7 +425,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provide the notification ringtone filename * - * @return the filename or null if "None" is selected + * @return the filename or null if "NoCondition" is selected */ fun getNotificationRingToneName(): String? { val toneUri = getNotificationRingTone() ?: return null diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt index 904407708f..bfe08a5c52 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt @@ -22,7 +22,7 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams +import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.matrix.rx.rx import im.vector.riotx.ActiveSessionDataSource import im.vector.riotx.core.platform.EmptyAction @@ -60,10 +60,11 @@ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: } private fun observeRoomSummaries() { + val queryParams = roomSummaryQueryParams() sessionObservableStore.observe() .observeOn(AndroidSchedulers.mainThread()) .switchMap { - it.orNull()?.rx()?.liveRoomSummaries(RoomSummaryQueryParams()) + it.orNull()?.rx()?.liveRoomSummaries(queryParams) ?: Observable.just(emptyList()) } .throttleLast(300, TimeUnit.MILLISECONDS) From e14b9b3b20bf90ec82e3c65741c888e24427e4e1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Jan 2020 08:03:14 +0100 Subject: [PATCH 103/105] Fix test compilation issue --- .../matrix/android/api/pushrules/PushrulesConditionTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt index a29f5d5542..1d1bbe1406 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import io.mockk.every import io.mockk.mockk @@ -40,7 +40,7 @@ class PushrulesConditionTest { content = MessageTextContent("m.text", "Yo wtf?").toContent(), originServerTs = 0) - val rm = RoomMember( + val rm = RoomMemberContent( Membership.INVITE, displayName = "Foo", avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf" @@ -72,7 +72,7 @@ class PushrulesConditionTest { type = "m.room.member", eventId = "mx0", stateKey = "@foo:matrix.org", - content = RoomMember( + content = RoomMemberContent( Membership.INVITE, displayName = "Foo", avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf" From 0f7d59a8c786efc4006a6cdf95b236f742060be4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Jan 2020 09:42:34 +0100 Subject: [PATCH 104/105] Cleanup during PR review --- .../android/api/session/group/GroupService.kt | 2 +- .../room/model/relation/RelationService.kt | 2 +- .../internal/query/QueryEnumListProcessor.kt | 8 ++++---- .../query/QueryStringValueProcessor.kt | 18 +++++++----------- .../room/membership/RoomMemberEventHandler.kt | 2 +- .../group/AutocompleteGroupPresenter.kt | 5 ----- .../member/AutocompleteMemberPresenter.kt | 8 -------- .../features/home/room/detail/AutoCompleter.kt | 6 +++--- .../room/detail/timeline/item/BaseEventItem.kt | 1 - .../features/settings/VectorPreferences.kt | 4 ++-- .../res/layout/item_bottom_sheet_action.xml | 2 +- 11 files changed, 20 insertions(+), 38 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt index 76bac1ae1a..c01e5b5cd8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt @@ -39,7 +39,7 @@ interface GroupService { fun getGroupSummary(groupId: String): GroupSummary? /** - * Get a list of group summaries.This list is a snapshot of the data. + * Get a list of group summaries. This list is a snapshot of the data. * @return the list of [GroupSummary] */ fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index d13d7db773..31ed4e9986 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -116,7 +116,7 @@ interface RelationService { fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? /** - * Get the a LiveData EventAnnotationsSummary + * Get a LiveData of EventAnnotationsSummary for the specified eventId * @param eventId the eventId to look for EventAnnotationsSummary * @return the LiveData of EventAnnotationsSummary */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt index 7cb5ca1047..2bc05eacec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt @@ -21,13 +21,13 @@ import io.realm.RealmQuery fun > RealmQuery.process(field: String, enums: List>): RealmQuery { val lastEnumValue = enums.lastOrNull() - this.beginGroup() + beginGroup() for (enumValue in enums) { - this.equalTo(field, enumValue.name) + equalTo(field, enumValue.name) if (enumValue != lastEnumValue) { - this.or() + or() } } - this.endGroup() + endGroup() return this } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt index d2418ee84f..ebe10cad9c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt @@ -25,16 +25,12 @@ import timber.log.Timber fun RealmQuery.process(field: String, queryStringValue: QueryStringValue): RealmQuery { when (queryStringValue) { is QueryStringValue.NoCondition -> Timber.v("No condition to process") - is QueryStringValue.IsNotNull -> this.isNotNull(field) - is QueryStringValue.IsNull -> this.isNull(field) - is QueryStringValue.IsEmpty -> this.isEmpty(field) - is QueryStringValue.IsNotEmpty -> this.isNotEmpty(field) - is QueryStringValue.Equals -> { - this.equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase()) - } - is QueryStringValue.Contains -> { - this.contains(field, queryStringValue.string, queryStringValue.case.toRealmCase()) - } + is QueryStringValue.IsNotNull -> isNotNull(field) + is QueryStringValue.IsNull -> isNull(field) + is QueryStringValue.IsEmpty -> isEmpty(field) + is QueryStringValue.IsNotEmpty -> isNotEmpty(field) + is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase()) } return this } @@ -42,6 +38,6 @@ fun RealmQuery.process(field: String, queryStringValue: Que private fun QueryStringValue.Case.toRealmCase(): Case { return when (this) { QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE - QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE + QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt index 16d6aacbc9..9bd97cec10 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt @@ -35,7 +35,7 @@ internal class RoomMemberEventHandler @Inject constructor() { val userId = event.stateKey ?: return false val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) realm.insertOrUpdate(roomMemberEntity) - if (roomMember.membership == Membership.JOIN || roomMember.membership == Membership.INVITE) { + if (roomMember.membership in Membership.activeMemberships()) { val userEntity = UserEntityFactory.create(userId, roomMember) realm.insertOrUpdate(userEntity) } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt index 8a726a81b1..b6f45b477c 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.autocomplete.group import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.airbnb.mvrx.Async import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session @@ -59,8 +58,4 @@ class AutocompleteGroupPresenter @Inject constructor(context: Context, .sortedBy { it.displayName } controller.setData(groups.toList()) } - - fun render(groups: Async>) { - controller.setData(groups()) - } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt index 5bf573ecb1..84a33173b8 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -18,8 +18,6 @@ package im.vector.riotx.features.autocomplete.member import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Success import com.otaliastudios.autocomplete.RecyclerViewPresenter import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -71,10 +69,4 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, .sortedBy { it.displayName } controller.setData(members.toList()) } - - fun render(members: Async>) { - if (members is Success) { - controller.setData(members()) - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 6ae937fed2..7ca647ea3e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -50,7 +50,7 @@ class AutoCompleter @AssistedInject constructor( private val avatarRenderer: AvatarRenderer, private val commandAutocompletePolicy: CommandAutocompletePolicy, private val autocompleteCommandPresenter: AutocompleteCommandPresenter, - private val autocompleteMemberPresenter: AutocompleteMemberPresenter.Factory, + private val autocompleteMemberPresenterFactory: AutocompleteMemberPresenter.Factory, private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val autocompleteGroupPresenter: AutocompleteGroupPresenter, private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter @@ -107,10 +107,10 @@ class AutoCompleter @AssistedInject constructor( } private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) { - val membersPresenter = autocompleteMemberPresenter.create(roomId) + val autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId) Autocomplete.on(editText) .with(CharPolicy('@', true)) - .with(membersPresenter) + .with(autocompleteMemberPresenter) .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 5db87fff0f..02b7341c72 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -27,7 +27,6 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.platform.CheckableView import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DimensionConverter -import kotlinx.coroutines.* /** * Children must override getViewType() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 774e3741e6..0ec67789fe 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -398,7 +398,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { fun getNotificationRingTone(): Uri? { val url = defaultPrefs.getString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, null) - // the user selects "NoCondition" + // the user selects "None" if (url == "") { return null } @@ -425,7 +425,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provide the notification ringtone filename * - * @return the filename or null if "NoCondition" is selected + * @return the filename or null if "None" is selected */ fun getNotificationRingToneName(): String? { val toneUri = getNotificationRingTone() ?: return null diff --git a/vector/src/main/res/layout/item_bottom_sheet_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml index db01db0a2f..66a096799d 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_action.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml @@ -47,12 +47,12 @@ android:maxLines="2" android:textColor="?riotx_text_secondary" android:textSize="17sp" + app:layout_constrainedWidth="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/actionSelected" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toEndOf="@id/actionStartSpace" app:layout_constraintTop_toTopOf="parent" - app:layout_constrainedWidth="true" tools:text="zbla azjazjaz s sdkqdskdsqk kqsdkdqsk kdqsksqdk" /> From bd4a595f96746bc31e6db0db8ec54edb529939ec Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Jan 2020 11:19:08 +0100 Subject: [PATCH 105/105] ChunkEntityTest: make it compile again --- .../session/room/timeline/ChunkEntityTest.kt | 40 +++---------------- 1 file changed, 6 insertions(+), 34 deletions(-) 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 5cca9f6696..3980094175 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -21,7 +21,6 @@ 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.isUnlinked import im.vector.matrix.android.internal.database.helper.lastStateIndex import im.vector.matrix.android.internal.database.helper.merge import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -33,7 +32,6 @@ import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeR import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject -import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldEqual import org.junit.Before @@ -150,30 +148,6 @@ internal class ChunkEntityTest : InstrumentedTest { } } - @Test - fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() { - monarchy.runTransactionSync { realm -> - val chunk1: ChunkEntity = realm.createObject() - val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) - chunk1.isUnlinked().shouldBeFalse() - } - } - - @Test - fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() { - monarchy.runTransactionSync { realm -> - val chunk1: ChunkEntity = realm.createObject() - val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) - chunk1.isUnlinked().shouldBeTrue() - } - } - @Test fun merge_shouldPrevTokenMerged_whenMergingForwards() { monarchy.runTransactionSync { realm -> @@ -181,8 +155,8 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val prevToken = "prev_token" chunk1.prevToken = prevToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS) chunk1.prevToken shouldEqual prevToken } @@ -195,8 +169,8 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val nextToken = "next_token" chunk1.nextToken = nextToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.nextToken shouldEqual nextToken } @@ -205,11 +179,9 @@ internal class ChunkEntityTest : InstrumentedTest { private fun ChunkEntity.addAll(roomId: String, events: List, direction: PaginationDirection, - stateIndexOffset: Int = 0, - // Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk) - isUnlinked: Boolean = false) { + stateIndexOffset: Int = 0) { events.forEach { event -> - add(roomId, event, direction, stateIndexOffset, isUnlinked) + add(roomId, event, direction, stateIndexOffset) } } }