mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:30:43 +00:00
Merge branch 'develop' into feature/fga/timeline-multi-forward
This commit is contained in:
@@ -28,7 +28,7 @@ dependencies {
|
||||
} else {
|
||||
debugImplementation(libs.matrix.sdk)
|
||||
}
|
||||
implementation(projects.libraries.rustlsTls)
|
||||
implementation(variantOf(libs.rustls.platform.verifier) { artifactType("aar") })
|
||||
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.features.enterprise.api)
|
||||
|
||||
+47
-6
@@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
@@ -136,6 +137,7 @@ import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -143,6 +145,9 @@ import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParam
|
||||
import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
|
||||
import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
|
||||
|
||||
// Same value as AVATAR_THUMBNAIL_SIZE_IN_PIXEL in the matrixmedia module
|
||||
private const val AVATAR_THUMBNAIL_SIZE_IN_PIXEL = 240L
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class RustMatrixClient(
|
||||
override val sessionPaths: SessionPaths,
|
||||
@@ -289,7 +294,11 @@ class RustMatrixClient(
|
||||
|
||||
private val ownProfileListener = object : ProfileListener {
|
||||
override fun onUpdate(profile: UserProfile) {
|
||||
_userProfile.tryEmit(profile.map())
|
||||
val matrixUser = profile.map()
|
||||
_userProfile.tryEmit(matrixUser)
|
||||
sessionCoroutineScope.launch {
|
||||
storeUserProfile(matrixUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,13 +494,37 @@ class RustMatrixClient(
|
||||
.onSuccess { matrixUser ->
|
||||
// Also update our session storage
|
||||
_userProfile.emit(matrixUser)
|
||||
sessionStore.updateUserProfile(
|
||||
sessionId = sessionId.value,
|
||||
displayName = matrixUser.displayName,
|
||||
avatarUrl = matrixUser.avatarUrl,
|
||||
)
|
||||
storeUserProfile(matrixUser)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the user profile in the session storage, including a base64 encoded thumbnail of the avatar,
|
||||
* so that the avatar can be rendered without having to use this client.
|
||||
*/
|
||||
private suspend fun storeUserProfile(matrixUser: MatrixUser) {
|
||||
val avatarUrl = matrixUser.avatarUrl
|
||||
val storedSession = sessionStore.getSession(sessionId.value)
|
||||
val avatarData = when {
|
||||
avatarUrl == null -> null
|
||||
// Avatar has not changed, no need to download it again
|
||||
avatarUrl == storedSession?.userAvatarUrl && storedSession.userAvatarData != null -> storedSession.userAvatarData
|
||||
else -> matrixMediaLoader.loadMediaThumbnail(
|
||||
source = MediaSource(avatarUrl),
|
||||
width = AVATAR_THUMBNAIL_SIZE_IN_PIXEL,
|
||||
height = AVATAR_THUMBNAIL_SIZE_IN_PIXEL,
|
||||
)
|
||||
.map { Base64.encode(it) }
|
||||
.onFailure { Timber.w(it, "Unable to load the avatar thumbnail of the user") }
|
||||
.getOrNull()
|
||||
}
|
||||
sessionStore.updateUserProfile(
|
||||
sessionId = sessionId.value,
|
||||
displayName = matrixUser.displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
avatarData = avatarData,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
|
||||
withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
@@ -779,6 +812,14 @@ class RustMatrixClient(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRoomInfo(roomId: RoomId): Result<RoomInfo?> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoomListService.roomOrNull(roomId.value)?.use { room ->
|
||||
roomInfoMapper.map(room.roomInfo())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<RoomInfo>> {
|
||||
return mxCallbackFlow {
|
||||
val roomNotFound = innerRoomListService.roomOrNull(roomId.value).use { it == null }
|
||||
|
||||
-1
@@ -28,7 +28,6 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
|
||||
}
|
||||
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.EventCache -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.InvalidRawKey -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.WellKnownLookupDisabled -> AuthenticationException.Generic(message)
|
||||
}
|
||||
is OAuthException -> when (this) {
|
||||
|
||||
+3
@@ -381,6 +381,9 @@ class RustMatrixAuthenticationService(
|
||||
// Apply enterprise hooks to the newly created client as soon as possible
|
||||
clientEnterpriseHook(matrixClient)
|
||||
|
||||
// Start the sync service to ensure that the client receives the encryption updates we'll wait for next.
|
||||
matrixClient.syncService.startSync()
|
||||
|
||||
matrixClient.waitForKnownVerificationState()
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
|
||||
+1
@@ -38,4 +38,5 @@ internal fun Session.toSessionData(
|
||||
lastUsageIndex = 0,
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
userAvatarData = null,
|
||||
)
|
||||
|
||||
+17
@@ -8,10 +8,27 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.media
|
||||
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
||||
|
||||
fun RustMediaSource.map(): MediaSource = use {
|
||||
MediaSource(it.url(), it.toJson())
|
||||
}
|
||||
|
||||
internal fun MediaSource.toRustMediaSource(): RustMediaSource {
|
||||
val json = this.json
|
||||
return try {
|
||||
if (json != null) {
|
||||
RustMediaSource.fromJson(json)
|
||||
} else {
|
||||
RustMediaSource.fromUrl(safeUrl)
|
||||
}
|
||||
} catch (e: LinkageError) {
|
||||
// Used for tests, since we can't instantiate an actual `RustMediaSource` because the native library can't be loaded
|
||||
val isTest = runCatchingExceptions { Class.forName("org.junit.Test") }.isSuccess
|
||||
if (isTest) RustMediaSource(NoHandle) else throw e
|
||||
}
|
||||
}
|
||||
|
||||
-10
@@ -20,7 +20,6 @@ import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import java.io.File
|
||||
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
||||
|
||||
class RustMediaLoader(
|
||||
private val baseCacheDirectory: File,
|
||||
@@ -80,13 +79,4 @@ class RustMediaLoader(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaSource.toRustMediaSource(): RustMediaSource {
|
||||
val json = this.json
|
||||
return if (json != null) {
|
||||
RustMediaSource.fromJson(json)
|
||||
} else {
|
||||
RustMediaSource.fromUrl(safeUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -478,7 +478,7 @@ class JoinedRustRoom(
|
||||
widgetSettings = widgetSettings,
|
||||
room = innerRoom,
|
||||
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
|
||||
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
|
||||
override suspend fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
|
||||
return getElementCallRequiredPermissions(sessionId.value, baseRoom.deviceId.value)
|
||||
}
|
||||
},
|
||||
|
||||
+1
-17
@@ -12,10 +12,9 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.media.toRustMediaSource
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import org.matrix.rustcomponents.sdk.ContentScanner as RustScanner
|
||||
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
||||
|
||||
class RustContentScanner(
|
||||
private val client: Client,
|
||||
@@ -29,18 +28,3 @@ class RustContentScanner(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaSource.toRustMediaSource(): RustMediaSource {
|
||||
val json = this.json
|
||||
return try {
|
||||
if (json != null) {
|
||||
RustMediaSource.fromJson(json)
|
||||
} else {
|
||||
RustMediaSource.fromUrl(safeUrl)
|
||||
}
|
||||
} catch (e: LinkageError) {
|
||||
// Used for tests, since we can't instantiate an actual `RustMediaSource` because the native library can't be loaded
|
||||
val isTest = runCatchingExceptions { Class.forName("org.junit.Test") }.isSuccess
|
||||
if (isTest) RustMediaSource(NoHandle) else throw e
|
||||
}
|
||||
}
|
||||
|
||||
+10
-15
@@ -7,11 +7,13 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.storage
|
||||
|
||||
import android.util.Base64
|
||||
import io.element.android.libraries.androidutils.crypto.ClientSecret
|
||||
import io.element.android.libraries.core.data.ByteUnit
|
||||
import io.element.android.libraries.core.data.megaBytes
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
import uniffi.matrix_sdk_sqlite.Base64Variant
|
||||
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder as SdkSqliteStoreBuilder
|
||||
|
||||
/**
|
||||
@@ -40,10 +42,15 @@ class RustSqliteStoreBuilder(
|
||||
override fun secret(clientSecret: ClientSecret?): SqliteStoreBuilder {
|
||||
when (clientSecret) {
|
||||
null -> Unit
|
||||
is ClientSecret.Passphrase -> inner = inner.passphrase(clientSecret.value)
|
||||
is ClientSecret.Passphrase -> {
|
||||
// Use high entropy passphrase to migrate a passphrase to a raw key.
|
||||
val base64Passphrase = Base64.decode(clientSecret.value, Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
inner = inner.highEntropyPassphrase(base64Passphrase, Base64Variant.UNPADDED)
|
||||
// Once the passphrase is migrated, we can derive a raw key from it and use that for the SDK, replacing the line above.
|
||||
// inner = inner.key(base64Passphrase)
|
||||
}
|
||||
is ClientSecret.RawKey -> {
|
||||
// Ensure the key is 32 bytes long, as required by the SDK
|
||||
inner = inner.key(clientSecret.keyOfSize(32))
|
||||
inner = inner.key(clientSecret.bytes)
|
||||
}
|
||||
}
|
||||
return this
|
||||
@@ -53,15 +60,3 @@ class RustSqliteStoreBuilder(
|
||||
return clientBuilder.sqliteStore(this.inner)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClientSecret.RawKey.keyOfSize(size: Int): ByteArray {
|
||||
return if (bytes.size == 32) {
|
||||
bytes
|
||||
} else if (bytes.size < 32) {
|
||||
// If the key is shorter than 32 bytes, pad it with zeros
|
||||
bytes + ByteArray(32 - bytes.size)
|
||||
} else {
|
||||
// Otherwise, take the first 32 bytes of the key
|
||||
bytes.copyOfRange(0, 32)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -31,7 +31,6 @@ import org.matrix.rustcomponents.sdk.EventOrTransactionId
|
||||
import org.matrix.rustcomponents.sdk.QueueWedgeError
|
||||
import org.matrix.rustcomponents.sdk.Reaction
|
||||
import org.matrix.rustcomponents.sdk.ShieldState
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||
import uniffi.matrix_sdk_ui.TimelineEventShieldStateCode
|
||||
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
|
||||
@@ -52,7 +51,7 @@ class EventTimelineItemMapper(
|
||||
isOwn = isOwn,
|
||||
isRemote = isRemote,
|
||||
localSendState = localSendState?.map(),
|
||||
reactions = (content as? TimelineItemContent.MsgLike)?.content?.reactions.map(),
|
||||
reactions = reactions.map(),
|
||||
receipts = readReceipts.map(),
|
||||
sender = UserId(sender),
|
||||
senderProfile = senderProfile.map(),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package org.matrix.rustsdk
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* This class is used to initialize the Rust SDK on Android and give it access to an Android [Context].
|
||||
*/
|
||||
object Android {
|
||||
init {
|
||||
System.loadLibrary("matrix_sdk_ffi")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
external fun init(context: Context)
|
||||
}
|
||||
+1
-1
@@ -49,7 +49,7 @@ class RustMatrixClientFactoryTest {
|
||||
fun TestScope.createRustMatrixClientFactory(
|
||||
cacheDirectory: File = File("/cache"),
|
||||
sessionStore: SessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
updateUserProfileResult = { _, _, _, _ -> },
|
||||
),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||
|
||||
+151
-12
@@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.data.bytes
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
|
||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
||||
@@ -29,6 +30,7 @@ import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
@@ -39,9 +41,9 @@ import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.CreateRoomParameters
|
||||
import org.matrix.rustcomponents.sdk.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.RoomHistoryVisibility
|
||||
import org.matrix.rustcomponents.sdk.StoreSizes
|
||||
import org.matrix.rustcomponents.sdk.UserProfile
|
||||
import java.io.File
|
||||
|
||||
private const val AN_ACCOUNT_DATA_EVENT_TYPE = "org.example.custom"
|
||||
@@ -76,7 +78,8 @@ class RustMatrixClientTest {
|
||||
@Test
|
||||
fun `retrieving the UserProfile updates the database`() = runTest {
|
||||
val profilePersisted = CompletableDeferred<Unit>()
|
||||
val updateUserProfileResult = lambdaRecorder<String, String?, String?, Unit> { _, _, _ -> profilePersisted.complete(Unit) }
|
||||
val updateUserProfileResult = lambdaRecorder<String, String?, String?, String?, Unit> { _, _, _, _ -> profilePersisted.complete(Unit) }
|
||||
val getMediaThumbnailResult = lambdaRecorder<MediaSource, ULong, ULong, ByteArray> { _, _, _ -> byteArrayOf(1, 2, 3) }
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
@@ -89,24 +92,160 @@ class RustMatrixClientTest {
|
||||
)
|
||||
val client = createRustMatrixClient(
|
||||
client = FakeFfiClient(
|
||||
getProfileResult = { userId ->
|
||||
UserProfile(
|
||||
userId = userId,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
status = null,
|
||||
call = null,
|
||||
)
|
||||
},
|
||||
getProfileResult = { userId -> aRustUserProfile(userId = userId, displayName = A_USER_NAME, avatarUrl = AN_AVATAR_URL) },
|
||||
getMediaThumbnailResult = getMediaThumbnailResult,
|
||||
),
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
profilePersisted.await()
|
||||
getMediaThumbnailResult.assertions().isCalledOnce()
|
||||
.with(any(), value(240uL), value(240uL))
|
||||
updateUserProfileResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
value(A_USER_ID.value),
|
||||
value(A_USER_NAME),
|
||||
value(AN_AVATAR_URL),
|
||||
value("AQID"),
|
||||
)
|
||||
client.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retrieving the UserProfile does not download the avatar again if it has not changed`() = runTest {
|
||||
val profilePersisted = CompletableDeferred<Unit>()
|
||||
val updateUserProfileResult = lambdaRecorder<String, String?, String?, String?, Unit> { _, _, _, _ -> profilePersisted.complete(Unit) }
|
||||
val getMediaThumbnailResult = lambdaRecorder<MediaSource, ULong, ULong, ByteArray> { _, _, _ -> byteArrayOf(1, 2, 3) }
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
userDisplayName = A_USER_NAME,
|
||||
userAvatarUrl = AN_AVATAR_URL,
|
||||
userAvatarData = "storedData",
|
||||
)
|
||||
),
|
||||
updateUserProfileResult = updateUserProfileResult,
|
||||
)
|
||||
val client = createRustMatrixClient(
|
||||
client = FakeFfiClient(
|
||||
getProfileResult = { userId -> aRustUserProfile(userId = userId, displayName = A_USER_NAME, avatarUrl = AN_AVATAR_URL) },
|
||||
getMediaThumbnailResult = getMediaThumbnailResult,
|
||||
),
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
profilePersisted.await()
|
||||
getMediaThumbnailResult.assertions().isNeverCalled()
|
||||
updateUserProfileResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
value(A_USER_ID.value),
|
||||
value(A_USER_NAME),
|
||||
value(AN_AVATAR_URL),
|
||||
value("storedData"),
|
||||
)
|
||||
client.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retrieving the UserProfile downloads the avatar again if the previous download failed`() = runTest {
|
||||
val profilePersisted = CompletableDeferred<Unit>()
|
||||
val updateUserProfileResult = lambdaRecorder<String, String?, String?, String?, Unit> { _, _, _, _ -> profilePersisted.complete(Unit) }
|
||||
val getMediaThumbnailResult = lambdaRecorder<MediaSource, ULong, ULong, ByteArray> { _, _, _ -> byteArrayOf(1, 2, 3) }
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
userDisplayName = A_USER_NAME,
|
||||
userAvatarUrl = AN_AVATAR_URL,
|
||||
userAvatarData = null,
|
||||
)
|
||||
),
|
||||
updateUserProfileResult = updateUserProfileResult,
|
||||
)
|
||||
val client = createRustMatrixClient(
|
||||
client = FakeFfiClient(
|
||||
getProfileResult = { userId -> aRustUserProfile(userId = userId, displayName = A_USER_NAME, avatarUrl = AN_AVATAR_URL) },
|
||||
getMediaThumbnailResult = getMediaThumbnailResult,
|
||||
),
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
profilePersisted.await()
|
||||
getMediaThumbnailResult.assertions().isCalledOnce()
|
||||
updateUserProfileResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
value(A_USER_ID.value),
|
||||
value(A_USER_NAME),
|
||||
value(AN_AVATAR_URL),
|
||||
value("AQID"),
|
||||
)
|
||||
client.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retrieving the UserProfile clears the avatar data if the avatar download fails`() = runTest {
|
||||
val profilePersisted = CompletableDeferred<Unit>()
|
||||
val updateUserProfileResult = lambdaRecorder<String, String?, String?, String?, Unit> { _, _, _, _ -> profilePersisted.complete(Unit) }
|
||||
val getMediaThumbnailResult = lambdaRecorder<MediaSource, ULong, ULong, ByteArray> { _, _, _ -> error("Download failed") }
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
userDisplayName = A_USER_NAME,
|
||||
userAvatarUrl = "mxc://server.org/previousAvatar",
|
||||
userAvatarData = "previousData",
|
||||
)
|
||||
),
|
||||
updateUserProfileResult = updateUserProfileResult,
|
||||
)
|
||||
val client = createRustMatrixClient(
|
||||
client = FakeFfiClient(
|
||||
getProfileResult = { userId -> aRustUserProfile(userId = userId, displayName = A_USER_NAME, avatarUrl = AN_AVATAR_URL) },
|
||||
getMediaThumbnailResult = getMediaThumbnailResult,
|
||||
),
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
profilePersisted.await()
|
||||
getMediaThumbnailResult.assertions().isCalledOnce()
|
||||
updateUserProfileResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
value(A_USER_ID.value),
|
||||
value(A_USER_NAME),
|
||||
value(AN_AVATAR_URL),
|
||||
value(null),
|
||||
)
|
||||
client.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retrieving the UserProfile without avatar clears the avatar data`() = runTest {
|
||||
val profilePersisted = CompletableDeferred<Unit>()
|
||||
val updateUserProfileResult = lambdaRecorder<String, String?, String?, String?, Unit> { _, _, _, _ -> profilePersisted.complete(Unit) }
|
||||
val getMediaThumbnailResult = lambdaRecorder<MediaSource, ULong, ULong, ByteArray> { _, _, _ -> byteArrayOf(1, 2, 3) }
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
userDisplayName = A_USER_NAME,
|
||||
userAvatarUrl = AN_AVATAR_URL,
|
||||
userAvatarData = "storedData",
|
||||
)
|
||||
),
|
||||
updateUserProfileResult = updateUserProfileResult,
|
||||
)
|
||||
val client = createRustMatrixClient(
|
||||
client = FakeFfiClient(
|
||||
getProfileResult = { userId -> aRustUserProfile(userId = userId, displayName = A_USER_NAME, avatarUrl = null) },
|
||||
getMediaThumbnailResult = getMediaThumbnailResult,
|
||||
),
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
profilePersisted.await()
|
||||
getMediaThumbnailResult.assertions().isNeverCalled()
|
||||
updateUserProfileResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
value(A_USER_ID.value),
|
||||
value(A_USER_NAME),
|
||||
value(null),
|
||||
value(null),
|
||||
)
|
||||
client.destroy()
|
||||
}
|
||||
@@ -169,7 +308,7 @@ class RustMatrixClientTest {
|
||||
private fun TestScope.createRustMatrixClient(
|
||||
client: Client = FakeFfiClient(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
updateUserProfileResult = { _, _, _, _ -> },
|
||||
),
|
||||
) = RustMatrixClient(
|
||||
innerClient = client,
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ class RustTemporaryMatrixClientFactoryTest {
|
||||
private fun TestScope.createRustMatrixClientFactory(
|
||||
cacheDirectory: File = File("/cache"),
|
||||
sessionStore: SessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
updateUserProfileResult = { _, _, _, _ -> },
|
||||
),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||
|
||||
+1
-1
@@ -255,7 +255,7 @@ class RustMatrixAuthenticationServiceTest {
|
||||
}
|
||||
|
||||
private fun TestScope.createRustMatrixAuthenticationService(
|
||||
sessionStore: SessionStore = InMemorySessionStore(updateUserProfileResult = { _, _, _ -> }),
|
||||
sessionStore: SessionStore = InMemorySessionStore(updateUserProfileResult = { _, _, _, _ -> }),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||
sessionPathsFactory: SessionPathsFactory = SessionPathsFactory(File("/base"), File("/cache")),
|
||||
|
||||
+7
@@ -16,6 +16,7 @@ import org.matrix.rustcomponents.sdk.EventSendState
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.LazyTimelineItemProvider
|
||||
import org.matrix.rustcomponents.sdk.ProfileDetails
|
||||
import org.matrix.rustcomponents.sdk.Reaction
|
||||
import org.matrix.rustcomponents.sdk.Receipt
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||
import uniffi.matrix_sdk_ui.EventItemOrigin
|
||||
@@ -37,7 +38,10 @@ internal fun aRustEventTimelineItem(
|
||||
readReceipts: Map<String, Receipt> = emptyMap(),
|
||||
origin: EventItemOrigin? = EventItemOrigin.SYNC,
|
||||
canBeRepliedTo: Boolean = true,
|
||||
reactions: List<Reaction> = emptyList(),
|
||||
lazyProvider: LazyTimelineItemProvider = FakeFfiLazyTimelineItemProvider(),
|
||||
editSendState: EventSendState? = null,
|
||||
redactionSendState: EventSendState? = null,
|
||||
) = EventTimelineItem(
|
||||
isRemote = isRemote,
|
||||
eventOrTransactionId = eventOrTransactionId,
|
||||
@@ -55,5 +59,8 @@ internal fun aRustEventTimelineItem(
|
||||
readReceipts = readReceipts,
|
||||
origin = origin,
|
||||
canBeRepliedTo = canBeRepliedTo,
|
||||
reactions = reactions,
|
||||
lazyProvider = lazyProvider,
|
||||
editSendState = editSendState,
|
||||
redactionSendState = redactionSendState,
|
||||
)
|
||||
|
||||
-1
@@ -27,7 +27,6 @@ internal fun aRustTimelineItemContentMsgLike(
|
||||
mentions = null,
|
||||
)
|
||||
),
|
||||
reactions = emptyList(),
|
||||
threadRoot = null,
|
||||
inReplyTo = null,
|
||||
threadSummary = null,
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import org.matrix.rustcomponents.sdk.UserStatus
|
||||
internal fun aRustUserProfile(
|
||||
userId: String = A_USER_ID.value,
|
||||
displayName: String = "displayName",
|
||||
avatarUrl: String = "avatarUrl",
|
||||
avatarUrl: String? = "avatarUrl",
|
||||
status: UserStatus? = null,
|
||||
call: UserCall? = null,
|
||||
) = UserProfile(
|
||||
|
||||
+6
@@ -26,6 +26,7 @@ import org.matrix.rustcomponents.sdk.HomeserverCapabilities
|
||||
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
|
||||
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
||||
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
|
||||
import org.matrix.rustcomponents.sdk.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import org.matrix.rustcomponents.sdk.NotificationClient
|
||||
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
||||
@@ -70,6 +71,7 @@ class FakeFfiClient(
|
||||
private val isProfilesSlidingSyncExtensionSupportedResult: () -> Boolean = { false },
|
||||
private val subscribeToOwnProfileResult: (ProfileListener) -> Unit = {},
|
||||
private val getUrlResult: (String) -> ByteArray = { lambdaError() },
|
||||
private val getMediaThumbnailResult: (MediaSource, ULong, ULong) -> ByteArray = { _, _, _ -> byteArrayOf() },
|
||||
private val contentScannerResult: () -> ContentScanner = { FakeFfiContentScanner() },
|
||||
private val closeResult: () -> Unit = {},
|
||||
) : Client(NoHandle) {
|
||||
@@ -122,6 +124,10 @@ class FakeFfiClient(
|
||||
return getProfileResult(userId)
|
||||
}
|
||||
|
||||
override suspend fun getMediaThumbnail(mediaSource: MediaSource, width: ULong, height: ULong): ByteArray {
|
||||
return getMediaThumbnailResult(mediaSource, width, height)
|
||||
}
|
||||
|
||||
override suspend fun homeserverLoginDetails(): HomeserverLoginDetails {
|
||||
return homeserverLoginDetailsResult()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user