mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Store the user avatar thumbnail in the session database
Add a `userAvatarData` column to the session database, containing the base64 encoded thumbnail of the user avatar. It will allow rendering the avatar of an account without having to use its Matrix client, for instance on the signed out screen, or for other accounts when the homeservers are not federated. The thumbnail is downloaded when the user profile is retrieved or updated, and only if the avatar URL has changed. The profile is now also persisted when received from the own profile subscription. Part of #5571 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-4
@@ -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 {
|
||||
|
||||
+1
@@ -47,5 +47,6 @@ private fun aSessionData(
|
||||
lastUsageIndex = 0,
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
userAvatarData = null,
|
||||
)
|
||||
}
|
||||
|
||||
+39
-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 {
|
||||
|
||||
+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
-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
|
||||
}
|
||||
}
|
||||
|
||||
+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")),
|
||||
|
||||
+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()
|
||||
}
|
||||
|
||||
+2
@@ -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?,
|
||||
)
|
||||
|
||||
+2
-1
@@ -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.
|
||||
|
||||
+3
-1
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Binary file not shown.
+4
-1
@@ -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
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
+8
-2
@@ -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)
|
||||
|
||||
|
||||
+1
@@ -30,4 +30,5 @@ internal fun aDbSessionData(
|
||||
lastUsageIndex = 0,
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
userAvatarData = null,
|
||||
)
|
||||
|
||||
+3
-3
@@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.map
|
||||
|
||||
class InMemorySessionStore(
|
||||
initialList: List<SessionData> = 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? {
|
||||
|
||||
+2
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user