diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilder.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilder.kt index b29dc788a7..f9c5fc3bc0 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilder.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilder.kt @@ -36,6 +36,7 @@ class CurrentUserWithNeighborsBuilder { userId = UserId(it.userId), displayName = it.userDisplayName, avatarUrl = it.userAvatarUrl, + avatarThumbnail = it.userAvatarData, ) } } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt index 13c9585386..e8d041bca3 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt @@ -220,4 +220,30 @@ class CurrentUserWithNeighborsBuilderTest { ) ) } + + @Test + fun `other accounts use the data from db, including the avatar thumbnail`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser(id = A_USER_ID.value) + val list = listOf( + aSessionData( + sessionId = A_USER_ID.value, + ), + aSessionData( + sessionId = A_USER_ID_2.value, + userDisplayName = "Carol", + userAvatarUrl = "avatarUrl", + userAvatarData = "avatarData", + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result[0]).isEqualTo( + MatrixUser( + userId = A_USER_ID_2, + displayName = "Carol", + avatarUrl = "avatarUrl", + avatarThumbnail = "avatarData", + ) + ) + } } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index 371a718523..58ad4eb100 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -81,7 +81,7 @@ class HomePresenterTest { val presenter = createHomePresenter( rageshakeFeatureAvailability = { flowOf(true) }, sessionStore = InMemorySessionStore( - updateUserProfileResult = { _, _, _ -> }, + updateUserProfileResult = { _, _, _, _ -> }, ), ) presenter.test { @@ -98,7 +98,7 @@ class HomePresenterTest { val presenter = createHomePresenter( indicatorService = indicatorService, sessionStore = InMemorySessionStore( - updateUserProfileResult = { _, _, _ -> }, + updateUserProfileResult = { _, _, _, _ -> }, ), ) presenter.test { @@ -120,7 +120,7 @@ class HomePresenterTest { val presenter = createHomePresenter( client = matrixClient, sessionStore = InMemorySessionStore( - updateUserProfileResult = { _, _, _ -> }, + updateUserProfileResult = { _, _, _, _ -> }, ), ) presenter.test { @@ -134,7 +134,7 @@ class HomePresenterTest { fun `present - NavigationBar change`() = runTest { val presenter = createHomePresenter( sessionStore = InMemorySessionStore( - updateUserProfileResult = { _, _, _ -> }, + updateUserProfileResult = { _, _, _, _ -> }, ), ) presenter.test { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 6bada34e93..4bbe2eeaf8 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -90,6 +90,7 @@ class PreferencesRootPresenter( userId = UserId(it.userId), displayName = it.userDisplayName, avatarUrl = it.userAvatarUrl, + avatarThumbnail = it.userAvatarData, ) } .toImmutableList() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 3d438f7044..085b74e323 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -234,6 +234,7 @@ class PreferencesRootPresenterTest { sessionId = A_SESSION_ID_2.value, userDisplayName = "Bob", userAvatarUrl = "avatarUrl", + userAvatarData = "avatarData", ), ) ) @@ -241,7 +242,14 @@ class PreferencesRootPresenterTest { val state = awaitFirstItem() assertThat(state.isMultiAccountEnabled).isTrue() assertThat(state.otherSessions).hasSize(1) - assertThat(state.otherSessions[0]).isEqualTo(MatrixUser(userId = A_SESSION_ID_2, displayName = "Bob", avatarUrl = "avatarUrl")) + assertThat(state.otherSessions[0]).isEqualTo( + MatrixUser( + userId = A_SESSION_ID_2, + displayName = "Bob", + avatarUrl = "avatarUrl", + avatarThumbnail = "avatarData", + ) + ) } } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt index ae60b7aacb..e0396cba97 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt @@ -47,5 +47,6 @@ private fun aSessionData( lastUsageIndex = 0, userDisplayName = null, userAvatarUrl = null, + userAvatarData = null, ) } diff --git a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenter.kt b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenter.kt index be6a8bea6f..e1d6b2800e 100644 --- a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenter.kt +++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenter.kt @@ -34,6 +34,7 @@ class AccountSelectPresenter( userId = UserId(it.userId), displayName = it.userDisplayName, avatarUrl = it.userAvatarUrl, + avatarThumbnail = it.userAvatarData, ) } .toImmutableList() diff --git a/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenterTest.kt b/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenterTest.kt index 8b0f581f6d..f9b9aeebcd 100644 --- a/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenterTest.kt +++ b/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenterTest.kt @@ -44,6 +44,7 @@ class AccountSelectPresenterTest { sessionId = A_SESSION_ID_2.value, userDisplayName = "Bob", userAvatarUrl = "avatarUrl", + userAvatarData = "avatarData", ), ) ) @@ -66,6 +67,7 @@ class AccountSelectPresenterTest { userId = A_SESSION_ID_2, displayName = "Bob", avatarUrl = "avatarUrl", + avatarThumbnail = "avatarData", ) ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt index 99c45c67d9..88f8f3abb5 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt @@ -18,6 +18,8 @@ data class AvatarData( val name: String?, val url: String? = null, val size: AvatarSize, + /** Base64 encoded thumbnail of [url], stored locally, used as a fallback when the avatar cannot be loaded. */ + val thumbnail: String? = null, ) { val initialLetter by lazy { // For roomIds, use "#" as initial diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt index 53ef5593ed..ee069f71fd 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt @@ -16,9 +16,11 @@ fun anAvatarData( name: String? = USER_NAME_ALICE, url: String? = null, size: AvatarSize = AvatarSize.RoomListItem, + thumbnail: String? = null, ) = AvatarData( id = id, name = name, url = url, size = size, + thumbnail = thumbnail, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt index da57fbcbe1..e8dcd94a77 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Shape @@ -21,8 +22,10 @@ import androidx.compose.ui.unit.Dp import coil3.compose.AsyncImagePainter import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImageContent +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.designsystem.components.avatar.AvatarData import timber.log.Timber +import kotlin.io.encoding.Base64 @Composable internal fun ImageAvatar( @@ -33,6 +36,9 @@ internal fun ImageAvatar( contentDescription: String? = null, ) { val size = forcedAvatarSize ?: avatarData.size.dp + val thumbnailBytes = remember(avatarData.thumbnail) { + avatarData.thumbnail?.let { runCatchingExceptions { Base64.decode(it) }.getOrNull() } + } SubcomposeAsyncImage( model = avatarData, contentDescription = contentDescription, @@ -51,15 +57,17 @@ internal fun ImageAvatar( "Error loading avatar $state\n${state.result}" ) } - InitialLetterAvatar( + ThumbnailOrInitialLetterAvatar( avatarData = avatarData, + thumbnailBytes = thumbnailBytes, avatarShape = avatarShape, forcedAvatarSize = forcedAvatarSize, contentDescription = contentDescription, ) } - else -> InitialLetterAvatar( + else -> ThumbnailOrInitialLetterAvatar( avatarData = avatarData, + thumbnailBytes = thumbnailBytes, avatarShape = avatarShape, forcedAvatarSize = forcedAvatarSize, contentDescription = contentDescription, @@ -67,3 +75,44 @@ internal fun ImageAvatar( } } } + +/** + * Render the locally stored thumbnail of the avatar if any, else the initial letter. + */ +@Composable +private fun ThumbnailOrInitialLetterAvatar( + avatarData: AvatarData, + thumbnailBytes: ByteArray?, + avatarShape: Shape, + forcedAvatarSize: Dp?, + contentDescription: String?, +) { + if (thumbnailBytes == null) { + InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = forcedAvatarSize, + contentDescription = contentDescription, + ) + } else { + SubcomposeAsyncImage( + model = thumbnailBytes, + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(forcedAvatarSize ?: avatarData.size.dp) + .clip(avatarShape) + ) { + val collectedState by painter.state.collectAsState() + when (collectedState) { + is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent() + else -> InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = forcedAvatarSize, + contentDescription = contentDescription, + ) + } + } + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixUser.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixUser.kt index 7a8ec739a5..f27557bda2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixUser.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixUser.kt @@ -19,4 +19,6 @@ data class MatrixUser( val avatarUrl: String? = null, val displayedStatus: DisplayedStatus? = null, val rawStatus: UserStatus? = null, + /** Base64 encoded thumbnail of [avatarUrl], stored locally, used as a fallback when the avatar cannot be loaded. */ + val avatarThumbnail: String? = null, ) : Parcelable diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 86414ed6a3..d92509b262 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -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 = withContext(sessionDispatcher) { runCatchingExceptions { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt index 69a76f2825..57f2b8c597 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt @@ -38,4 +38,5 @@ internal fun Session.toSessionData( lastUsageIndex = 0, userDisplayName = null, userAvatarUrl = null, + userAvatarData = null, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt index 08bef24722..c42e9c1f02 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt @@ -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 + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index 7b3a1d9f10..0b8229cb6b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -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) - } - } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScanner.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScanner.kt index c07ca94f26..60ae423f98 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScanner.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScanner.kt @@ -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 - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index b559b7d4d0..4dcb8b1df8 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -49,7 +49,7 @@ class RustMatrixClientFactoryTest { fun TestScope.createRustMatrixClientFactory( cacheDirectory: File = File("/cache"), sessionStore: SessionStore = InMemorySessionStore( - updateUserProfileResult = { _, _, _ -> }, + updateUserProfileResult = { _, _, _, _ -> }, ), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt index ea399b39df..4fe61e4d14 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt @@ -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() - val updateUserProfileResult = lambdaRecorder { _, _, _ -> profilePersisted.complete(Unit) } + val updateUserProfileResult = lambdaRecorder { _, _, _, _ -> profilePersisted.complete(Unit) } + val getMediaThumbnailResult = lambdaRecorder { _, _, _ -> 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() + val updateUserProfileResult = lambdaRecorder { _, _, _, _ -> profilePersisted.complete(Unit) } + val getMediaThumbnailResult = lambdaRecorder { _, _, _ -> 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() + val updateUserProfileResult = lambdaRecorder { _, _, _, _ -> profilePersisted.complete(Unit) } + val getMediaThumbnailResult = lambdaRecorder { _, _, _ -> 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() + val updateUserProfileResult = lambdaRecorder { _, _, _, _ -> profilePersisted.complete(Unit) } + val getMediaThumbnailResult = lambdaRecorder { _, _, _ -> 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() + val updateUserProfileResult = lambdaRecorder { _, _, _, _ -> profilePersisted.complete(Unit) } + val getMediaThumbnailResult = lambdaRecorder { _, _, _ -> 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, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt index f18f633889..f4a229b30b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt @@ -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(), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt index d81c6021da..9b589b2281 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt @@ -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")), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt index 947e6e7aa1..07997e9109 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt @@ -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( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt index 8c6411197a..c2080747d5 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -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() } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt index 83a707347d..ac3cdf0e3b 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt @@ -20,6 +20,7 @@ fun MatrixUser.getAvatarData(size: AvatarSize) = AvatarData( name = displayName, url = avatarUrl, size = size, + thumbnail = avatarThumbnail, ) fun MatrixUser.getBestName(): String { diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt index 035548a4ec..ae8a04b7cd 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt @@ -33,12 +33,14 @@ class MatrixUserExtensionsTest : RobolectricTest() { userId = A_USER_ID, displayName = "displayName", avatarUrl = "avatarUrl", + avatarThumbnail = "avatarThumbnail", ) val expected = AvatarData( id = A_USER_ID.value, name = "displayName", url = "avatarUrl", size = AvatarSize.UserHeader, + thumbnail = "avatarThumbnail", ) assertThat(matrixUser.getAvatarData(AvatarSize.UserHeader)).isEqualTo(expected) } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt index 88cf9558b3..fa4e22232d 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt @@ -46,4 +46,6 @@ data class SessionData( val userDisplayName: String?, /** The optional avatar URL of the user. */ val userAvatarUrl: String?, + /** The optional thumbnail of the avatar of the user ([userAvatarUrl]), base64 encoded. */ + val userAvatarData: String?, ) diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index 641a2afdf2..a84a1f00dc 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -51,8 +51,9 @@ interface SessionStore { * @param sessionId the session to update. * @param displayName the new display name, or `null` when the user has none. * @param avatarUrl the new avatar, or `null` when the user has none. + * @param avatarData the thumbnail of the new avatar, base64 encoded, or `null` when the user has none or when it could not be loaded. */ - suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) + suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?, avatarData: String?) /** * Get the session data matching the userId, or null if not found. diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index 9be59e4bae..f183c42225 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -84,12 +84,13 @@ class DatabaseSessionStore( lastUsageIndex = result.lastUsageIndex, userDisplayName = result.userDisplayName, userAvatarUrl = result.userAvatarUrl, + userAvatarData = result.userAvatarData, ).toDbModel() ) } } - override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) { + override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?, avatarData: String?) { sessionDataMutex.withLock { val result = database.sessionDataQueries.selectByUserId(sessionId) .executeAsOneOrNull() @@ -102,6 +103,7 @@ class DatabaseSessionStore( result.copy( userDisplayName = displayName, userAvatarUrl = avatarUrl, + userAvatarData = avatarData, ).toDbModel() ) } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index 316bf86c1c..0c55d46f66 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -31,6 +31,7 @@ internal fun SessionData.toDbModel(): DbSessionData { lastUsageIndex = lastUsageIndex, userDisplayName = userDisplayName, userAvatarUrl = userAvatarUrl, + userAvatarData = userAvatarData, ) } @@ -52,5 +53,6 @@ internal fun DbSessionData.toApiModel(): SessionData { lastUsageIndex = lastUsageIndex, userDisplayName = userDisplayName, userAvatarUrl = userAvatarUrl, + userAvatarData = userAvatarData, ) } diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/13.db b/libraries/session-storage/impl/src/main/sqldelight/databases/13.db new file mode 100644 index 0000000000..e2d2bebe6a Binary files /dev/null and b/libraries/session-storage/impl/src/main/sqldelight/databases/13.db differ diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index 8e5fb7a1f8..869bca34ed 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -36,7 +36,10 @@ CREATE TABLE SessionData ( -- user display name userDisplayName TEXT, -- user avatar url - userAvatarUrl TEXT + userAvatarUrl TEXT, + -- added in version 13 + -- user avatar thumbnail data, base64 encoded + userAvatarData TEXT ); diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/12.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/12.sqm new file mode 100644 index 0000000000..f6ba7fb28a --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/12.sqm @@ -0,0 +1,4 @@ +-- Migrate DB from version 12 +-- Add the thumbnail of the user avatar, base64 encoded, so that the avatar can be rendered without a Matrix client. + +ALTER TABLE SessionData ADD COLUMN userAvatarData TEXT; diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt index e40000986b..cc91ee8039 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt @@ -134,17 +134,18 @@ class DatabaseSessionStoreTest { @Test fun `updateUserProfile does nothing if the session is not found`() = runTest { - databaseSessionStore.updateUserProfile(aSessionData.userId, "userDisplayName", "userAvatarUrl") + databaseSessionStore.updateUserProfile(aSessionData.userId, "userDisplayName", "userAvatarUrl", "userAvatarData") assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull() } @Test fun `updateUserProfile update the data`() = runTest { database.sessionDataQueries.insertSessionData(aSessionData) - databaseSessionStore.updateUserProfile(aSessionData.userId, "userDisplayName", "userAvatarUrl") + databaseSessionStore.updateUserProfile(aSessionData.userId, "userDisplayName", "userAvatarUrl", "userAvatarData") val updatedSession = database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne() assertThat(updatedSession.userDisplayName).isEqualTo("userDisplayName") assertThat(updatedSession.userAvatarUrl).isEqualTo("userAvatarUrl") + assertThat(updatedSession.userAvatarData).isEqualTo("userAvatarData") } @Test @@ -231,6 +232,7 @@ class DatabaseSessionStoreTest { lastUsageIndex = 0, userDisplayName = "userDisplayName", userAvatarUrl = "userAvatarUrl", + userAvatarData = "userAvatarData", ) val secondSessionData = SessionData( userId = "userId", @@ -249,6 +251,7 @@ class DatabaseSessionStoreTest { lastUsageIndex = 1, userDisplayName = "userDisplayNameAltered", userAvatarUrl = "userAvatarUrlAltered", + userAvatarData = "userAvatarDataAltered", ) assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId) assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp) @@ -273,6 +276,7 @@ class DatabaseSessionStoreTest { assertThat(alteredSession.lastUsageIndex).isEqualTo(firstSessionData.lastUsageIndex) assertThat(alteredSession.userDisplayName).isEqualTo(firstSessionData.userDisplayName) assertThat(alteredSession.userAvatarUrl).isEqualTo(firstSessionData.userAvatarUrl) + assertThat(alteredSession.userAvatarData).isEqualTo(firstSessionData.userAvatarData) } @Test @@ -294,6 +298,7 @@ class DatabaseSessionStoreTest { lastUsageIndex = 0, userDisplayName = "userDisplayName", userAvatarUrl = "userAvatarUrl", + userAvatarData = "userAvatarData", ) val secondSessionData = SessionData( userId = "userIdUnknown", @@ -312,6 +317,7 @@ class DatabaseSessionStoreTest { lastUsageIndex = 1, userDisplayName = "userDisplayNameAltered", userAvatarUrl = "userAvatarUrlAltered", + userAvatarData = "userAvatarDataAltered", ) assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId) diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt index 4a488588ff..a6292631e9 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt @@ -30,4 +30,5 @@ internal fun aDbSessionData( lastUsageIndex = 0, userDisplayName = null, userAvatarUrl = null, + userAvatarData = null, ) diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt index 05e58ac623..ac54c1f1d4 100644 --- a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt +++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.map class InMemorySessionStore( initialList: List = emptyList(), - private val updateUserProfileResult: (String, String?, String?) -> Unit = { _, _, _ -> error("Not implemented") }, + private val updateUserProfileResult: (String, String?, String?, String?) -> Unit = { _, _, _, _ -> error("Not implemented") }, private val setLatestSessionResult: (String) -> Unit = { error("Not implemented") }, ) : SessionStore { private val sessionDataListFlow = MutableStateFlow(initialList) @@ -56,8 +56,8 @@ class InMemorySessionStore( } } - override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) { - updateUserProfileResult(sessionId, displayName, avatarUrl) + override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?, avatarData: String?) { + updateUserProfileResult(sessionId, displayName, avatarUrl, avatarData) } override suspend fun getSession(sessionId: String): SessionData? { diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt index c5acd77755..c7c1856f8c 100644 --- a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt +++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt @@ -23,6 +23,7 @@ fun aSessionData( lastUsageIndex: Long = 0, userDisplayName: String? = null, userAvatarUrl: String? = null, + userAvatarData: String? = null, ): SessionData { return SessionData( userId = sessionId, @@ -41,5 +42,6 @@ fun aSessionData( lastUsageIndex = lastUsageIndex, userDisplayName = userDisplayName, userAvatarUrl = userAvatarUrl, + userAvatarData = userAvatarData, ) }