Merge branch 'develop' into feature/fga/pinned_messages_list

This commit is contained in:
ganfra 2024-09-04 14:11:53 +02:00 committed by GitHub
commit 12e7e05551
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
289 changed files with 3558 additions and 1883 deletions

View file

@ -0,0 +1,133 @@
/*
* Copyright (c) 2024 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
*
* https://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 io.element.android.libraries.matrix.impl
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.ClientSessionDelegate
import org.matrix.rustcomponents.sdk.Session
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
/**
* This class is responsible for handling the session data for the Rust SDK.
*
* It implements both [ClientSessionDelegate] and [ClientDelegate] to react to session data updates and auth errors.
*
* IMPORTANT: you must set the [client] property as soon as possible so [didReceiveAuthError] can work properly.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class RustClientSessionDelegate(
private val sessionStore: SessionStore,
private val appCoroutineScope: CoroutineScope,
coroutineDispatchers: CoroutineDispatchers,
) : ClientSessionDelegate, ClientDelegate {
private val clientLog = Timber.tag("$this")
// Used to ensure several calls to `didReceiveAuthError` don't trigger multiple logouts
private val isLoggingOut = AtomicBoolean(false)
// To make sure only one coroutine affecting the token persistence can run at a time
private val updateTokensDispatcher = coroutineDispatchers.io.limitedParallelism(1)
// This Client needs to be set up as soon as possible so `didReceiveAuthError` can work properly.
private var client: RustMatrixClient? = null
/**
* Sets the [ClientDelegate] for the [RustMatrixClient], and keeps a reference to the client so it can be used later.
*/
fun bindClient(client: RustMatrixClient) {
this.client = client
client.setDelegate(this)
}
override fun saveSessionInKeychain(session: Session) {
appCoroutineScope.launch(updateTokensDispatcher) {
val existingData = sessionStore.getSession(session.userId) ?: return@launch
val (anonymizedAccessToken, anonymizedRefreshToken) = session.anonymizedTokens()
clientLog.d(
"Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " +
"Was token valid: ${existingData.isTokenValid}"
)
val newData = session.toSessionData(
isTokenValid = true,
loginType = existingData.loginType,
passphrase = existingData.passphrase,
sessionPaths = existingData.getSessionPaths(),
)
sessionStore.updateData(newData)
clientLog.d("Saved new session data with access token: '$anonymizedAccessToken'.")
}.invokeOnCompletion {
if (it != null) {
clientLog.e(it, "Failed to save new session data.")
}
}
}
override fun didReceiveAuthError(isSoftLogout: Boolean) {
clientLog.w("didReceiveAuthError(isSoftLogout=$isSoftLogout)")
if (isLoggingOut.getAndSet(true).not()) {
clientLog.v("didReceiveAuthError -> do the cleanup")
// TODO handle isSoftLogout parameter.
appCoroutineScope.launch(updateTokensDispatcher) {
val currentClient = client
if (currentClient == null) {
clientLog.w("didReceiveAuthError -> no client, exiting")
isLoggingOut.set(false)
return@launch
}
val existingData = sessionStore.getSession(currentClient.sessionId.value)
val (anonymizedAccessToken, anonymizedRefreshToken) = existingData.anonymizedTokens()
clientLog.d(
"Removing session data with access token '$anonymizedAccessToken' " +
"and refresh token '$anonymizedRefreshToken'."
)
if (existingData != null) {
// Set isTokenValid to false
val newData = existingData.copy(isTokenValid = false)
sessionStore.updateData(newData)
clientLog.d("Invalidated session data with access token: '$anonymizedAccessToken'.")
} else {
clientLog.d("No session data found.")
}
client?.logout(userInitiated = false, ignoreSdkError = true)
}.invokeOnCompletion {
if (it != null) {
clientLog.e(it, "Failed to remove session data.")
}
}
} else {
clientLog.v("didReceiveAuthError -> already cleaning up")
}
}
override fun didRefreshTokens() {
// This is done in `saveSessionInKeychain(Session)` instead.
}
override fun retrieveSessionFromKeychain(userId: String): Session {
// This should never be called, as it's only used for multi-process setups
error("retrieveSessionFromKeychain should never be called for Android")
}
}

View file

@ -51,7 +51,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
@ -67,8 +66,7 @@ import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.util.SessionDirectoryProvider
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
@ -99,7 +97,6 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.BackupState
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.ClientException
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
@ -110,7 +107,6 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.io.File
import java.util.Optional
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration
import kotlin.time.Duration.Companion.INFINITE
import kotlin.time.Duration.Companion.seconds
@ -129,6 +125,7 @@ class RustMatrixClient(
private val baseDirectory: File,
baseCacheDirectory: File,
private val clock: SystemClock,
sessionDelegate: RustClientSessionDelegate,
) : MatrixClient {
override val sessionId: UserId = UserId(client.userId())
override val deviceId: String = client.deviceId()
@ -137,8 +134,6 @@ class RustMatrixClient(
private val innerRoomListService = syncService.roomListService()
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
// To make sure only one coroutine affecting the token persistence can run at a time
private val tokenRefreshDispatcher = sessionDispatcher.limitedParallelism(1)
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
private val pushersService = RustPushersService(
client = client,
@ -161,73 +156,7 @@ class RustMatrixClient(
sessionDispatcher = sessionDispatcher,
)
private val sessionDirectoryProvider = SessionDirectoryProvider(sessionStore)
private val isLoggingOut = AtomicBoolean(false)
private val clientDelegate = object : ClientDelegate {
private val clientLog get() = Timber.tag(this@RustMatrixClient.toString())
override fun didReceiveAuthError(isSoftLogout: Boolean) {
clientLog.w("didReceiveAuthError(isSoftLogout=$isSoftLogout)")
if (isLoggingOut.getAndSet(true).not()) {
clientLog.v("didReceiveAuthError -> do the cleanup")
// TODO handle isSoftLogout parameter.
appCoroutineScope.launch(tokenRefreshDispatcher) {
val existingData = sessionStore.getSession(client.userId())
val (anonymizedAccessToken, anonymizedRefreshToken) = existingData.anonymizedTokens()
clientLog.d(
"Removing session data with access token '$anonymizedAccessToken' " +
"and refresh token '$anonymizedRefreshToken'."
)
if (existingData != null) {
// Set isTokenValid to false
val newData = client.session().toSessionData(
isTokenValid = false,
loginType = existingData.loginType,
passphrase = existingData.passphrase,
sessionPath = existingData.sessionPath,
)
sessionStore.updateData(newData)
clientLog.d("Removed session data with access token: '$anonymizedAccessToken'.")
} else {
clientLog.d("No session data found.")
}
doLogout(doRequest = false, removeSession = false, ignoreSdkError = false)
}.invokeOnCompletion {
if (it != null) {
clientLog.e(it, "Failed to remove session data.")
}
}
} else {
clientLog.v("didReceiveAuthError -> already cleaning up")
}
}
override fun didRefreshTokens() {
clientLog.w("didRefreshTokens()")
appCoroutineScope.launch(tokenRefreshDispatcher) {
val existingData = sessionStore.getSession(client.userId()) ?: return@launch
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
clientLog.d(
"Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " +
"Was token valid: ${existingData.isTokenValid}"
)
val newData = client.session().toSessionData(
isTokenValid = true,
loginType = existingData.loginType,
passphrase = existingData.passphrase,
sessionPath = existingData.sessionPath,
)
sessionStore.updateData(newData)
clientLog.d("Saved new session data with access token: '$anonymizedAccessToken'.")
}.invokeOnCompletion {
if (it != null) {
clientLog.e(it, "Failed to save new session data.")
}
}
}
}
private val sessionPathsProvider = SessionPathsProvider(sessionStore)
private val roomSyncSubscriber: RoomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers)
@ -270,7 +199,7 @@ class RustMatrixClient(
private val roomMembershipObserver = RoomMembershipObserver()
private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(clientDelegate)
private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(sessionDelegate)
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(
MatrixUser(
@ -294,6 +223,9 @@ class RustMatrixClient(
.stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf())
init {
// Make sure the session delegate has a reference to the client to be able to logout on auth error
sessionDelegate.bindClient(this)
sessionCoroutineScope.launch {
// Force a refresh of the profile
getUserProfile()
@ -535,21 +467,11 @@ class RustMatrixClient(
deleteSessionDirectory(deleteCryptoDb = false)
}
override suspend fun logout(ignoreSdkError: Boolean): String? = doLogout(
doRequest = true,
removeSession = true,
ignoreSdkError = ignoreSdkError,
)
private suspend fun doLogout(
doRequest: Boolean,
removeSession: Boolean,
ignoreSdkError: Boolean,
): String? {
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? {
var result: String? = null
syncService.stop()
withContext(sessionDispatcher) {
if (doRequest) {
if (userInitiated) {
try {
result = client.logout()
} catch (failure: Throwable) {
@ -563,7 +485,7 @@ class RustMatrixClient(
}
close()
deleteSessionDirectory(deleteCryptoDb = true)
if (removeSession) {
if (userInitiated) {
sessionStore.removeSession(sessionId.value)
}
}
@ -614,19 +536,24 @@ class RustMatrixClient(
})
}.buffer(Channel.UNLIMITED)
internal fun setDelegate(delegate: RustClientSessionDelegate) {
client.setDelegate(delegate)
}
private suspend fun File.getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {
val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext 0L
val sessionDirectory = sessionPathsProvider.provides(sessionId) ?: return@withContext 0L
val cacheSize = sessionDirectory.cacheDirectory.getSizeOfFiles()
if (includeCryptoDb) {
sessionDirectory.getSizeOfFiles()
cacheSize + sessionDirectory.fileDirectory.getSizeOfFiles()
} else {
listOf(
cacheSize + listOf(
"matrix-sdk-state.sqlite3",
"matrix-sdk-state.sqlite3-shm",
"matrix-sdk-state.sqlite3-wal",
).map { fileName ->
File(sessionDirectory, fileName)
File(sessionDirectory.fileDirectory, fileName)
}.sumOf { file ->
file.length()
}
@ -636,14 +563,16 @@ class RustMatrixClient(
private suspend fun deleteSessionDirectory(
deleteCryptoDb: Boolean = false,
): Boolean = withContext(sessionDispatcher) {
val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext false
val sessionPaths = sessionPathsProvider.provides(sessionId) ?: return@withContext false
// Always delete the cache directory
sessionPaths.cacheDirectory.deleteRecursively()
if (deleteCryptoDb) {
// Delete the folder and all its content
sessionDirectory.deleteRecursively()
sessionPaths.fileDirectory.deleteRecursively()
} else {
// Delete only the state.db file
sessionDirectory.listFiles().orEmpty()
.filter { it.name.contains("matrix-sdk-state") }
// Do not delete the crypto database files.
sessionPaths.fileDirectory.listFiles().orEmpty()
.filterNot { it.name.contains("matrix-sdk-crypto") }
.forEach { file ->
Timber.w("Deleting file ${file.name}...")
file.safeDelete()

View file

@ -16,10 +16,13 @@
package io.element.android.libraries.matrix.impl
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider
@ -32,6 +35,8 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.io.File
@ -51,17 +56,20 @@ class RustMatrixClientFactory @Inject constructor(
private val appPreferencesStore: AppPreferencesStore,
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
val client = getBaseClientBuilder(
sessionPath = sessionData.sessionPath,
sessionPaths = sessionData.getSessionPaths(),
passphrase = sessionData.passphrase,
slidingSync = if (appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()) {
ClientBuilderSlidingSync.Simplified
} else {
ClientBuilderSlidingSync.Restored
},
slidingSync = when {
AuthenticationConfig.SLIDING_SYNC_PROXY_URL != null -> ClientBuilderSlidingSync.CustomProxy(AuthenticationConfig.SLIDING_SYNC_PROXY_URL!!)
appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first() -> ClientBuilderSlidingSync.Simplified
else -> ClientBuilderSlidingSync.Restored
}
)
.homeserverUrl(sessionData.homeserverUrl)
.username(sessionData.userId)
.setSessionDelegate(sessionDelegate)
.use { it.build() }
client.restoreSession(sessionData.toSession())
@ -81,31 +89,34 @@ class RustMatrixClientFactory @Inject constructor(
baseDirectory = baseDirectory,
baseCacheDirectory = cacheDirectory,
clock = clock,
sessionDelegate = sessionDelegate,
).also {
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
}
}
internal fun getBaseClientBuilder(
sessionPath: String,
sessionPaths: SessionPaths,
passphrase: String?,
slidingSyncProxy: String? = null,
slidingSync: ClientBuilderSlidingSync,
): ClientBuilder {
return ClientBuilder()
// TODO SDK claims it's valid to use the same path for data and cache, but would be better to use different paths
.sessionPaths(dataPath = sessionPath, cachePath = sessionPath)
.sessionPaths(
dataPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.absolutePath,
)
.passphrase(passphrase)
.slidingSyncProxy(slidingSyncProxy)
.userAgent(userAgentProvider.provide())
.addRootCertificates(userCertificatesProvider.provides())
.autoEnableBackups(true)
.autoEnableCrossSigning(true)
.run {
// Apply sliding sync version settings
when (slidingSync) {
ClientBuilderSlidingSync.Restored -> this
ClientBuilderSlidingSync.Discovered -> requiresSlidingSync()
ClientBuilderSlidingSync.Simplified -> simplifiedSlidingSync(true)
is ClientBuilderSlidingSync.CustomProxy -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Proxy(slidingSync.url))
ClientBuilderSlidingSync.Discovered -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DiscoverProxy)
ClientBuilderSlidingSync.Simplified -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Native)
}
}
.run {
@ -115,15 +126,18 @@ class RustMatrixClientFactory @Inject constructor(
}
}
enum class ClientBuilderSlidingSync {
sealed interface ClientBuilderSlidingSync {
// The proxy is set by the user.
data class CustomProxy(val url: String) : ClientBuilderSlidingSync
// The proxy will be supplied when restoring the Session.
Restored,
data object Restored : ClientBuilderSlidingSync
// A proxy must be discovered whilst building the session.
Discovered,
data object Discovered : ClientBuilderSlidingSync
// Use Simplified Sliding Sync (discovery isn't a thing yet).
Simplified,
data object Simplified : ClientBuilderSlidingSync
}
private fun SessionData.toSession() = Session(
@ -132,6 +146,6 @@ private fun SessionData.toSession() = Session(
userId = userId,
deviceId = deviceId,
homeserverUrl = homeserverUrl,
slidingSyncProxy = slidingSyncProxy,
slidingSyncVersion = slidingSyncProxy?.let(SlidingSyncVersion::Proxy) ?: SlidingSyncVersion.Native,
oidcData = oidcData,
)

View file

@ -24,7 +24,7 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
return when (this) {
is RustAuthenticationException.Generic -> AuthenticationException.Generic(message)
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(message)
is RustAuthenticationException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(message)
else -> AuthenticationException.Generic(message)
}
}

View file

@ -17,7 +17,6 @@
package io.element.android.libraries.matrix.impl.auth
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.di.AppScope
@ -37,6 +36,8 @@ import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionStore
@ -53,14 +54,12 @@ import org.matrix.rustcomponents.sdk.QrLoginProgressListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk.OidcAuthorizationData
import java.io.File
import java.util.UUID
import javax.inject.Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class RustMatrixAuthenticationService @Inject constructor(
private val baseDirectory: File,
private val sessionPathsFactory: SessionPathsFactory,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
private val rustMatrixClientFactory: RustMatrixClientFactory,
@ -73,14 +72,14 @@ class RustMatrixAuthenticationService @Inject constructor(
// Need to keep a copy of the current session path to eventually delete it.
// Ideally it would be possible to get the sessionPath from the Client to avoid doing this.
private var sessionPath: File? = null
private var sessionPaths: SessionPaths? = null
private var currentClient: Client? = null
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private fun rotateSessionPath(): File {
sessionPath?.deleteRecursively()
return File(baseDirectory, UUID.randomUUID().toString())
.also { sessionPath = it }
private fun rotateSessionPath(): SessionPaths {
sessionPaths?.deleteRecursively()
return sessionPathsFactory.create()
.also { sessionPaths = it }
}
override fun loggedInStateFlow(): Flow<LoggedInState> {
@ -145,14 +144,14 @@ class RustMatrixAuthenticationService @Inject constructor(
withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPath = sessionPath ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.login(username, password, "Element X Android", null)
val sessionData = client.session()
.toSessionData(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = pendingPassphrase,
sessionPath = currentSessionPath.absolutePath,
sessionPaths = currentSessionPaths,
)
clear()
sessionStore.storeData(sessionData)
@ -196,14 +195,14 @@ class RustMatrixAuthenticationService @Inject constructor(
return withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPath = sessionPath ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
val urlForOidcLogin = pendingOidcAuthorizationData ?: error("You need to call `getOidcUrl()` first")
client.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
val sessionData = client.session().toSessionData(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
sessionPath = currentSessionPath.absolutePath,
sessionPaths = currentSessionPaths,
)
clear()
pendingOidcAuthorizationData?.close()
@ -218,12 +217,11 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
val emptySessionPath = rotateSessionPath()
val emptySessionPaths = rotateSessionPath()
runCatching {
val client = rustMatrixClientFactory.getBaseClientBuilder(
sessionPath = emptySessionPath.absolutePath,
sessionPaths = emptySessionPaths,
passphrase = pendingPassphrase,
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
slidingSync = ClientBuilderSlidingSync.Discovered,
)
.buildWithQrCode(
@ -242,7 +240,7 @@ class RustMatrixAuthenticationService @Inject constructor(
isTokenValid = true,
loginType = LoginType.QR,
passphrase = pendingPassphrase,
sessionPath = emptySessionPath.absolutePath,
sessionPaths = emptySessionPaths,
)
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
@ -262,12 +260,11 @@ class RustMatrixAuthenticationService @Inject constructor(
}
private fun getBaseClientBuilder(
sessionPath: File,
sessionPaths: SessionPaths,
) = rustMatrixClientFactory
.getBaseClientBuilder(
sessionPath = sessionPath.absolutePath,
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
slidingSync = ClientBuilderSlidingSync.Discovered,
)

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.encryption
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.BackupState
@ -204,9 +205,9 @@ internal class RustEncryptionService(
override suspend fun startIdentityReset(): Result<IdentityResetHandle?> {
return runCatching {
service.resetIdentity()?.let { handle ->
RustIdentityResetHandleFactory.create(sessionId, handle)
}?.getOrNull()
service.resetIdentity()
}.flatMap { handle ->
RustIdentityResetHandleFactory.create(sessionId, handle)
}
}
}

View file

@ -27,13 +27,15 @@ import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType
object RustIdentityResetHandleFactory {
fun create(
userId: UserId,
identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle
): Result<IdentityResetHandle> {
identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle?
): Result<IdentityResetHandle?> {
return runCatching {
when (val authType = identityResetHandle.authType()) {
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
// User interactive authentication (user + password)
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
identityResetHandle?.let {
when (val authType = identityResetHandle.authType()) {
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
// User interactive authentication (user + password)
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
}
}
}
}

View file

@ -16,16 +16,18 @@
package io.element.android.libraries.matrix.impl.mapper
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
import java.util.Date
internal fun Session.toSessionData(
isTokenValid: Boolean,
loginType: LoginType,
passphrase: String?,
sessionPath: String,
sessionPaths: SessionPaths,
homeserverUrl: String? = null,
) = SessionData(
userId = userId,
@ -34,10 +36,11 @@ internal fun Session.toSessionData(
refreshToken = refreshToken,
homeserverUrl = homeserverUrl ?: this.homeserverUrl,
oidcData = oidcData,
slidingSyncProxy = slidingSyncProxy,
slidingSyncProxy = (slidingSyncVersion as? SlidingSyncVersion.Proxy)?.url,
loginTimestamp = Date(),
isTokenValid = isTokenValid,
loginType = loginType,
passphrase = passphrase,
sessionPath = sessionPath,
sessionPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.absolutePath,
)

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.notification.NotificationContent
@ -94,7 +95,10 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
is MessageLikeEventContent.RoomMessage -> {
NotificationContent.MessageLike.RoomMessage(senderId, EventMessageMapper().mapMessageType(messageType))
}
is MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction(redactedEventId = redactedEventId, reason = reason)
is MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction(
redactedEventId = redactedEventId?.let(::EventId),
reason = reason,
)
MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker
is MessageLikeEventContent.Poll -> NotificationContent.MessageLike.Poll(senderId, question)
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2024 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
*
* https://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 io.element.android.libraries.matrix.impl.paths
import io.element.android.libraries.sessionstorage.api.SessionData
import java.io.File
data class SessionPaths(
val fileDirectory: File,
val cacheDirectory: File,
) {
fun deleteRecursively() {
fileDirectory.deleteRecursively()
cacheDirectory.deleteRecursively()
}
}
internal fun SessionData.getSessionPaths(): SessionPaths {
return SessionPaths(
fileDirectory = File(sessionPath),
cacheDirectory = File(cachePath),
)
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2024 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
*
* https://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 io.element.android.libraries.matrix.impl.paths
import io.element.android.libraries.di.CacheDirectory
import java.io.File
import java.util.UUID
import javax.inject.Inject
class SessionPathsFactory @Inject constructor(
private val baseDirectory: File,
@CacheDirectory private val cacheDirectory: File,
) {
fun create(): SessionPaths {
val subPath = UUID.randomUUID().toString()
return SessionPaths(
fileDirectory = File(baseDirectory, subPath),
cacheDirectory = File(cacheDirectory, subPath),
)
}
}

View file

@ -45,6 +45,7 @@ class RoomSyncSubscriber(
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
RequiredState(key = EventType.STATE_ROOM_PINNED_EVENT, value = ""),
),
timelineLimit = DEFAULT_TIMELINE_LIMIT,
// We don't need heroes here as they're already included in the `all_rooms` list

View file

@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
@ -459,8 +460,8 @@ class RustMatrixRoom(
return liveTimeline.sendFile(file, fileInfo, progressCallback)
}
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> {
return liveTimeline.toggleReaction(emoji, eventId)
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> {
return liveTimeline.toggleReaction(emoji, uniqueId)
}
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> {

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
@ -31,7 +32,7 @@ class MatrixTimelineItemMapper(
private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
) {
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
val uniqueId = timelineItem.uniqueId()
val uniqueId = UniqueId(timelineItem.uniqueId())
val asEvent = it.asEvent()
if (asEvent != null) {
val eventTimelineItem = eventTimelineItemMapper.map(asEvent)

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
@ -391,9 +392,9 @@ class RustTimeline(
}
}
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> = withContext(dispatcher) {
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> = withContext(dispatcher) {
runCatching {
inner.toggleReaction(key = emoji, eventId = eventId.value)
inner.toggleReaction(key = emoji, uniqueId = uniqueId.value)
}
}

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
@ -26,7 +27,7 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
class LastForwardIndicatorsPostProcessor(
private val mode: Timeline.Mode,
) {
private val lastForwardIdentifiers = LinkedHashSet<String>()
private val lastForwardIdentifiers = LinkedHashSet<UniqueId>()
fun process(
items: List<MatrixTimelineItem>,
@ -57,17 +58,17 @@ class LastForwardIndicatorsPostProcessor(
}
}
private fun createLastForwardIndicator(identifier: String): MatrixTimelineItem {
private fun createLastForwardIndicator(identifier: UniqueId): MatrixTimelineItem {
return MatrixTimelineItem.Virtual(
uniqueId = "last_forward_indicator_$identifier",
uniqueId = UniqueId("last_forward_indicator_$identifier"),
virtual = VirtualTimelineItem.LastForwardIndicator
)
}
private fun List<MatrixTimelineItem>.latestEventIdentifier(): String {
private fun List<MatrixTimelineItem>.latestEventIdentifier(): UniqueId {
return findLast {
it is MatrixTimelineItem.Event
}?.let {
(it as MatrixTimelineItem.Event).uniqueId
} ?: "fake_id"
} ?: UniqueId("fake_id")
}

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
@ -33,7 +34,7 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
return buildList {
if (shouldAddBackwardLoadingIndicator) {
val backwardLoadingIndicator = MatrixTimelineItem.Virtual(
uniqueId = "BackwardLoadingIndicator",
uniqueId = UniqueId("BackwardLoadingIndicator"),
virtual = VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.BACKWARDS,
timestamp = currentTimestamp
@ -44,7 +45,7 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
addAll(items)
if (shouldAddForwardLoadingIndicator) {
val forwardLoadingIndicator = MatrixTimelineItem.Virtual(
uniqueId = "ForwardLoadingIndicator",
uniqueId = UniqueId("ForwardLoadingIndicator"),
virtual = VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.FORWARDS,
timestamp = currentTimestamp

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import androidx.annotation.VisibleForTesting
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
@ -79,7 +80,7 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) {
@VisibleForTesting
fun createRoomBeginningItem(): MatrixTimelineItem.Virtual {
return MatrixTimelineItem.Virtual(
uniqueId = VirtualTimelineItem.RoomBeginning.toString(),
uniqueId = UniqueId("RoomBeginning"),
virtual = VirtualTimelineItem.RoomBeginning
)
}

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import kotlinx.coroutines.CoroutineDispatcher
@ -23,6 +24,8 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.Date
internal val encryptedHistoryBannerId = UniqueId("EncryptedHistoryBannerId")
class TimelineEncryptedHistoryPostProcessor(
private val dispatcher: CoroutineDispatcher,
private val lastLoginTimestamp: Date?,
@ -44,7 +47,7 @@ class TimelineEncryptedHistoryPostProcessor(
}
return if (lastEncryptedHistoryBannerIndex >= 0) {
val sublist = list.drop(lastEncryptedHistoryBannerIndex + 1).toMutableList()
sublist.add(0, MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner))
sublist.add(0, MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner))
sublist
} else {
list

View file

@ -17,15 +17,15 @@
package io.element.android.libraries.matrix.impl.util
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.sessionstorage.api.SessionStore
import java.io.File
import javax.inject.Inject
class SessionDirectoryProvider @Inject constructor(
class SessionPathsProvider(
private val sessionStore: SessionStore,
) {
suspend fun provides(sessionId: SessionId): File? {
val path = sessionStore.getSession(sessionId.value)?.sessionPath ?: return null
return File(path)
suspend fun provides(sessionId: SessionId): SessionPaths? {
val sessionData = sessionStore.getSession(sessionId.value) ?: return null
return sessionData.getSessionPaths()
}
}

View file

@ -48,8 +48,8 @@ class AuthenticationExceptionMappingTest {
assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException())
.isException<AuthenticationException.Generic>("SDK issue")
assertThat(ClientBuildException.SlidingSyncNotAvailable("Sliding sync not available").mapAuthenticationException())
.isException<AuthenticationException.SlidingSyncNotAvailable>("Sliding sync not available")
assertThat(ClientBuildException.SlidingSyncVersion("Sliding sync not available").mapAuthenticationException())
.isException<AuthenticationException.SlidingSyncVersion>("Sliding sync not available")
}
private inline fun <reified T> ThrowableSubject.isException(message: String) {

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
@ -34,8 +35,8 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor removes room creation event and self-join event from DM timeline`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
@ -45,14 +46,20 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor removes room creation event and self-join event from DM timeline even if they're not the first items`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event(
UniqueId("m.room.member_other"),
anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))
),
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event(UniqueId("m.room.message"), anEventTimelineItem(content = aMessageContent("hi"))),
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val expected = listOf(
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
MatrixTimelineItem.Event(
UniqueId("m.room.member_other"),
anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))
),
MatrixTimelineItem.Event(UniqueId("m.room.message"), anEventTimelineItem(content = aMessageContent("hi"))),
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
@ -62,8 +69,8 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor will add beginning of room item if it's not a DM`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
@ -75,7 +82,7 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor will not add beginning of room item if it's not a DM and EncryptedHistoryBanner item is found`() {
val timelineItems = listOf(
MatrixTimelineItem.Virtual("EncryptedHistoryBanner", VirtualTimelineItem.EncryptedHistoryBanner),
MatrixTimelineItem.Virtual(UniqueId("EncryptedHistoryBanner"), VirtualTimelineItem.EncryptedHistoryBanner),
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
@ -85,8 +92,8 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor won't remove items if it's not at the start of the timeline`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
@ -96,7 +103,7 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor won't remove the first member join event if it can't find the room creation event`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
@ -106,8 +113,11 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor won't remove the first member join event if it's not from the room creator`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event(
UniqueId("m.room.member"),
anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))
),
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@ -26,8 +27,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.Date
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
class TimelineEncryptedHistoryPostProcessorTest {
private val defaultLastLoginTimestamp = Date(1_689_061_264L)
@ -35,7 +34,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
fun `given an unencrypted room, nothing is done`() = runTest {
val processor = createPostProcessor(isRoomEncrypted = false)
val items = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@ -44,7 +43,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
fun `given an encrypted room, and key backup enabled, nothing is done`() = runTest {
val processor = createPostProcessor(isKeyBackupEnabled = true)
val items = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@ -53,7 +52,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
fun `given a null lastLoginTimestamp, nothing is done`() = runTest {
val processor = createPostProcessor(lastLoginTimestamp = null)
val items = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@ -69,7 +68,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
fun `given a list with no items before lastLoginTimestamp, nothing is done`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@ -78,20 +77,20 @@ class TimelineEncryptedHistoryPostProcessorTest {
fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time))
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time))
)
assertThat(processor.process(items))
.isEqualTo(listOf(MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner)))
.isEqualTo(listOf(MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner)))
}
@Test
fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1))
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1))
)
assertThat(processor.process(items)).isEqualTo(
listOf(MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner))
listOf(MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner))
)
}
@ -99,14 +98,14 @@ class TimelineEncryptedHistoryPostProcessorTest {
fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, then they're replaced`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)),
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)),
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)),
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)),
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)),
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)),
)
assertThat(processor.process(items)).isEqualTo(
listOf(
MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner),
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner),
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
)
)
}