mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Merge pull request #7793 from element-hq/feature/bma/storeUserAvatar
Display the avatar of other accounts even if it cannot be loaded from the current account
This commit is contained in:
+1
@@ -36,6 +36,7 @@ class CurrentUserWithNeighborsBuilder {
|
|||||||
userId = UserId(it.userId),
|
userId = UserId(it.userId),
|
||||||
displayName = it.userDisplayName,
|
displayName = it.userDisplayName,
|
||||||
avatarUrl = it.userAvatarUrl,
|
avatarUrl = it.userAvatarUrl,
|
||||||
|
avatarThumbnail = it.userAvatarData,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -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",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -81,7 +81,7 @@ class HomePresenterTest {
|
|||||||
val presenter = createHomePresenter(
|
val presenter = createHomePresenter(
|
||||||
rageshakeFeatureAvailability = { flowOf(true) },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
sessionStore = InMemorySessionStore(
|
sessionStore = InMemorySessionStore(
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _, _ -> },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
@@ -98,7 +98,7 @@ class HomePresenterTest {
|
|||||||
val presenter = createHomePresenter(
|
val presenter = createHomePresenter(
|
||||||
indicatorService = indicatorService,
|
indicatorService = indicatorService,
|
||||||
sessionStore = InMemorySessionStore(
|
sessionStore = InMemorySessionStore(
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _, _ -> },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
@@ -120,7 +120,7 @@ class HomePresenterTest {
|
|||||||
val presenter = createHomePresenter(
|
val presenter = createHomePresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
sessionStore = InMemorySessionStore(
|
sessionStore = InMemorySessionStore(
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _, _ -> },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
@@ -134,7 +134,7 @@ class HomePresenterTest {
|
|||||||
fun `present - NavigationBar change`() = runTest {
|
fun `present - NavigationBar change`() = runTest {
|
||||||
val presenter = createHomePresenter(
|
val presenter = createHomePresenter(
|
||||||
sessionStore = InMemorySessionStore(
|
sessionStore = InMemorySessionStore(
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _, _ -> },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|||||||
+1
@@ -90,6 +90,7 @@ class PreferencesRootPresenter(
|
|||||||
userId = UserId(it.userId),
|
userId = UserId(it.userId),
|
||||||
displayName = it.userDisplayName,
|
displayName = it.userDisplayName,
|
||||||
avatarUrl = it.userAvatarUrl,
|
avatarUrl = it.userAvatarUrl,
|
||||||
|
avatarThumbnail = it.userAvatarData,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
|
|||||||
+9
-1
@@ -234,6 +234,7 @@ class PreferencesRootPresenterTest {
|
|||||||
sessionId = A_SESSION_ID_2.value,
|
sessionId = A_SESSION_ID_2.value,
|
||||||
userDisplayName = "Bob",
|
userDisplayName = "Bob",
|
||||||
userAvatarUrl = "avatarUrl",
|
userAvatarUrl = "avatarUrl",
|
||||||
|
userAvatarData = "avatarData",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -241,7 +242,14 @@ class PreferencesRootPresenterTest {
|
|||||||
val state = awaitFirstItem()
|
val state = awaitFirstItem()
|
||||||
assertThat(state.isMultiAccountEnabled).isTrue()
|
assertThat(state.isMultiAccountEnabled).isTrue()
|
||||||
assertThat(state.otherSessions).hasSize(1)
|
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",
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
@@ -47,5 +47,6 @@ private fun aSessionData(
|
|||||||
lastUsageIndex = 0,
|
lastUsageIndex = 0,
|
||||||
userDisplayName = null,
|
userDisplayName = null,
|
||||||
userAvatarUrl = null,
|
userAvatarUrl = null,
|
||||||
|
userAvatarData = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -34,6 +34,7 @@ class AccountSelectPresenter(
|
|||||||
userId = UserId(it.userId),
|
userId = UserId(it.userId),
|
||||||
displayName = it.userDisplayName,
|
displayName = it.userDisplayName,
|
||||||
avatarUrl = it.userAvatarUrl,
|
avatarUrl = it.userAvatarUrl,
|
||||||
|
avatarThumbnail = it.userAvatarData,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
|
|||||||
+2
@@ -44,6 +44,7 @@ class AccountSelectPresenterTest {
|
|||||||
sessionId = A_SESSION_ID_2.value,
|
sessionId = A_SESSION_ID_2.value,
|
||||||
userDisplayName = "Bob",
|
userDisplayName = "Bob",
|
||||||
userAvatarUrl = "avatarUrl",
|
userAvatarUrl = "avatarUrl",
|
||||||
|
userAvatarData = "avatarData",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -66,6 +67,7 @@ class AccountSelectPresenterTest {
|
|||||||
userId = A_SESSION_ID_2,
|
userId = A_SESSION_ID_2,
|
||||||
displayName = "Bob",
|
displayName = "Bob",
|
||||||
avatarUrl = "avatarUrl",
|
avatarUrl = "avatarUrl",
|
||||||
|
avatarThumbnail = "avatarData",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -18,6 +18,8 @@ data class AvatarData(
|
|||||||
val name: String?,
|
val name: String?,
|
||||||
val url: String? = null,
|
val url: String? = null,
|
||||||
val size: AvatarSize,
|
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 {
|
val initialLetter by lazy {
|
||||||
// For roomIds, use "#" as initial
|
// For roomIds, use "#" as initial
|
||||||
|
|||||||
+2
@@ -16,9 +16,11 @@ fun anAvatarData(
|
|||||||
name: String? = USER_NAME_ALICE,
|
name: String? = USER_NAME_ALICE,
|
||||||
url: String? = null,
|
url: String? = null,
|
||||||
size: AvatarSize = AvatarSize.RoomListItem,
|
size: AvatarSize = AvatarSize.RoomListItem,
|
||||||
|
thumbnail: String? = null,
|
||||||
) = AvatarData(
|
) = AvatarData(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
url = url,
|
url = url,
|
||||||
size = size,
|
size = size,
|
||||||
|
thumbnail = thumbnail,
|
||||||
)
|
)
|
||||||
|
|||||||
+51
-2
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
@@ -21,8 +22,10 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import coil3.compose.AsyncImagePainter
|
import coil3.compose.AsyncImagePainter
|
||||||
import coil3.compose.SubcomposeAsyncImage
|
import coil3.compose.SubcomposeAsyncImage
|
||||||
import coil3.compose.SubcomposeAsyncImageContent
|
import coil3.compose.SubcomposeAsyncImageContent
|
||||||
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ImageAvatar(
|
internal fun ImageAvatar(
|
||||||
@@ -33,6 +36,9 @@ internal fun ImageAvatar(
|
|||||||
contentDescription: String? = null,
|
contentDescription: String? = null,
|
||||||
) {
|
) {
|
||||||
val size = forcedAvatarSize ?: avatarData.size.dp
|
val size = forcedAvatarSize ?: avatarData.size.dp
|
||||||
|
val thumbnailBytes = remember(avatarData.thumbnail) {
|
||||||
|
avatarData.thumbnail?.let { runCatchingExceptions { Base64.decode(it) }.getOrNull() }
|
||||||
|
}
|
||||||
SubcomposeAsyncImage(
|
SubcomposeAsyncImage(
|
||||||
model = avatarData,
|
model = avatarData,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
@@ -51,15 +57,17 @@ internal fun ImageAvatar(
|
|||||||
"Error loading avatar $state\n${state.result}"
|
"Error loading avatar $state\n${state.result}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
InitialLetterAvatar(
|
ThumbnailOrInitialLetterAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
|
thumbnailBytes = thumbnailBytes,
|
||||||
avatarShape = avatarShape,
|
avatarShape = avatarShape,
|
||||||
forcedAvatarSize = forcedAvatarSize,
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> InitialLetterAvatar(
|
else -> ThumbnailOrInitialLetterAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
|
thumbnailBytes = thumbnailBytes,
|
||||||
avatarShape = avatarShape,
|
avatarShape = avatarShape,
|
||||||
forcedAvatarSize = forcedAvatarSize,
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
contentDescription = contentDescription,
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
@@ -19,4 +19,6 @@ data class MatrixUser(
|
|||||||
val avatarUrl: String? = null,
|
val avatarUrl: String? = null,
|
||||||
val displayedStatus: DisplayedStatus? = null,
|
val displayedStatus: DisplayedStatus? = null,
|
||||||
val rawStatus: UserStatus? = 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
|
) : Parcelable
|
||||||
|
|||||||
+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.LinkDesktopHandler
|
||||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
|
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.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.oauth.AccountManagementAction
|
||||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||||
@@ -136,6 +137,7 @@ import timber.log.Timber
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
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.RoomPreset as RustRoomPreset
|
||||||
import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
|
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")
|
@Suppress("LargeClass")
|
||||||
class RustMatrixClient(
|
class RustMatrixClient(
|
||||||
override val sessionPaths: SessionPaths,
|
override val sessionPaths: SessionPaths,
|
||||||
@@ -289,7 +294,11 @@ class RustMatrixClient(
|
|||||||
|
|
||||||
private val ownProfileListener = object : ProfileListener {
|
private val ownProfileListener = object : ProfileListener {
|
||||||
override fun onUpdate(profile: UserProfile) {
|
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 ->
|
.onSuccess { matrixUser ->
|
||||||
// Also update our session storage
|
// Also update our session storage
|
||||||
_userProfile.emit(matrixUser)
|
_userProfile.emit(matrixUser)
|
||||||
sessionStore.updateUserProfile(
|
storeUserProfile(matrixUser)
|
||||||
sessionId = sessionId.value,
|
|
||||||
displayName = matrixUser.displayName,
|
|
||||||
avatarUrl = matrixUser.avatarUrl,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> =
|
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
|
||||||
withContext(sessionDispatcher) {
|
withContext(sessionDispatcher) {
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
|
|||||||
+1
@@ -38,4 +38,5 @@ internal fun Session.toSessionData(
|
|||||||
lastUsageIndex = 0,
|
lastUsageIndex = 0,
|
||||||
userDisplayName = null,
|
userDisplayName = null,
|
||||||
userAvatarUrl = null,
|
userAvatarUrl = null,
|
||||||
|
userAvatarData = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+17
@@ -8,10 +8,27 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.media
|
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 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.use
|
||||||
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
||||||
|
|
||||||
fun RustMediaSource.map(): MediaSource = use {
|
fun RustMediaSource.map(): MediaSource = use {
|
||||||
MediaSource(it.url(), it.toJson())
|
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.Client
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
|
||||||
|
|
||||||
class RustMediaLoader(
|
class RustMediaLoader(
|
||||||
private val baseCacheDirectory: File,
|
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.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
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.exception.mapClientException
|
||||||
|
import io.element.android.libraries.matrix.impl.media.toRustMediaSource
|
||||||
import org.matrix.rustcomponents.sdk.Client
|
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.ContentScanner as RustScanner
|
||||||
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
|
||||||
|
|
||||||
class RustContentScanner(
|
class RustContentScanner(
|
||||||
private val client: Client,
|
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(
|
fun TestScope.createRustMatrixClientFactory(
|
||||||
cacheDirectory: File = File("/cache"),
|
cacheDirectory: File = File("/cache"),
|
||||||
sessionStore: SessionStore = InMemorySessionStore(
|
sessionStore: SessionStore = InMemorySessionStore(
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _, _ -> },
|
||||||
),
|
),
|
||||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
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.core.data.bytes
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
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.FakeFfiClient
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
|
||||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
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.libraries.workmanager.test.FakeWorkManagerScheduler
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
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.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
@@ -39,9 +41,9 @@ import kotlinx.coroutines.test.runTest
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.matrix.rustcomponents.sdk.Client
|
import org.matrix.rustcomponents.sdk.Client
|
||||||
import org.matrix.rustcomponents.sdk.CreateRoomParameters
|
import org.matrix.rustcomponents.sdk.CreateRoomParameters
|
||||||
|
import org.matrix.rustcomponents.sdk.MediaSource
|
||||||
import org.matrix.rustcomponents.sdk.RoomHistoryVisibility
|
import org.matrix.rustcomponents.sdk.RoomHistoryVisibility
|
||||||
import org.matrix.rustcomponents.sdk.StoreSizes
|
import org.matrix.rustcomponents.sdk.StoreSizes
|
||||||
import org.matrix.rustcomponents.sdk.UserProfile
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private const val AN_ACCOUNT_DATA_EVENT_TYPE = "org.example.custom"
|
private const val AN_ACCOUNT_DATA_EVENT_TYPE = "org.example.custom"
|
||||||
@@ -76,7 +78,8 @@ class RustMatrixClientTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `retrieving the UserProfile updates the database`() = runTest {
|
fun `retrieving the UserProfile updates the database`() = runTest {
|
||||||
val profilePersisted = CompletableDeferred<Unit>()
|
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(
|
val sessionStore = InMemorySessionStore(
|
||||||
initialList = listOf(
|
initialList = listOf(
|
||||||
aSessionData(
|
aSessionData(
|
||||||
@@ -89,24 +92,160 @@ class RustMatrixClientTest {
|
|||||||
)
|
)
|
||||||
val client = createRustMatrixClient(
|
val client = createRustMatrixClient(
|
||||||
client = FakeFfiClient(
|
client = FakeFfiClient(
|
||||||
getProfileResult = { userId ->
|
getProfileResult = { userId -> aRustUserProfile(userId = userId, displayName = A_USER_NAME, avatarUrl = AN_AVATAR_URL) },
|
||||||
UserProfile(
|
getMediaThumbnailResult = getMediaThumbnailResult,
|
||||||
userId = userId,
|
|
||||||
displayName = A_USER_NAME,
|
|
||||||
avatarUrl = AN_AVATAR_URL,
|
|
||||||
status = null,
|
|
||||||
call = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
)
|
)
|
||||||
profilePersisted.await()
|
profilePersisted.await()
|
||||||
|
getMediaThumbnailResult.assertions().isCalledOnce()
|
||||||
|
.with(any(), value(240uL), value(240uL))
|
||||||
updateUserProfileResult.assertions().isCalledOnce()
|
updateUserProfileResult.assertions().isCalledOnce()
|
||||||
.with(
|
.with(
|
||||||
value(A_USER_ID.value),
|
value(A_USER_ID.value),
|
||||||
value(A_USER_NAME),
|
value(A_USER_NAME),
|
||||||
value(AN_AVATAR_URL),
|
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()
|
client.destroy()
|
||||||
}
|
}
|
||||||
@@ -169,7 +308,7 @@ class RustMatrixClientTest {
|
|||||||
private fun TestScope.createRustMatrixClient(
|
private fun TestScope.createRustMatrixClient(
|
||||||
client: Client = FakeFfiClient(),
|
client: Client = FakeFfiClient(),
|
||||||
sessionStore: SessionStore = InMemorySessionStore(
|
sessionStore: SessionStore = InMemorySessionStore(
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _, _ -> },
|
||||||
),
|
),
|
||||||
) = RustMatrixClient(
|
) = RustMatrixClient(
|
||||||
innerClient = client,
|
innerClient = client,
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ class RustTemporaryMatrixClientFactoryTest {
|
|||||||
private fun TestScope.createRustMatrixClientFactory(
|
private fun TestScope.createRustMatrixClientFactory(
|
||||||
cacheDirectory: File = File("/cache"),
|
cacheDirectory: File = File("/cache"),
|
||||||
sessionStore: SessionStore = InMemorySessionStore(
|
sessionStore: SessionStore = InMemorySessionStore(
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _, _ -> },
|
||||||
),
|
),
|
||||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||||
|
|||||||
+1
-1
@@ -255,7 +255,7 @@ class RustMatrixAuthenticationServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createRustMatrixAuthenticationService(
|
private fun TestScope.createRustMatrixAuthenticationService(
|
||||||
sessionStore: SessionStore = InMemorySessionStore(updateUserProfileResult = { _, _, _ -> }),
|
sessionStore: SessionStore = InMemorySessionStore(updateUserProfileResult = { _, _, _, _ -> }),
|
||||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
sessionPathsFactory: SessionPathsFactory = SessionPathsFactory(File("/base"), File("/cache")),
|
sessionPathsFactory: SessionPathsFactory = SessionPathsFactory(File("/base"), File("/cache")),
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ import org.matrix.rustcomponents.sdk.UserStatus
|
|||||||
internal fun aRustUserProfile(
|
internal fun aRustUserProfile(
|
||||||
userId: String = A_USER_ID.value,
|
userId: String = A_USER_ID.value,
|
||||||
displayName: String = "displayName",
|
displayName: String = "displayName",
|
||||||
avatarUrl: String = "avatarUrl",
|
avatarUrl: String? = "avatarUrl",
|
||||||
status: UserStatus? = null,
|
status: UserStatus? = null,
|
||||||
call: UserCall? = null,
|
call: UserCall? = null,
|
||||||
) = UserProfile(
|
) = UserProfile(
|
||||||
|
|||||||
+6
@@ -26,6 +26,7 @@ import org.matrix.rustcomponents.sdk.HomeserverCapabilities
|
|||||||
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
|
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
|
||||||
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
||||||
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
|
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
|
||||||
|
import org.matrix.rustcomponents.sdk.MediaSource
|
||||||
import org.matrix.rustcomponents.sdk.NoHandle
|
import org.matrix.rustcomponents.sdk.NoHandle
|
||||||
import org.matrix.rustcomponents.sdk.NotificationClient
|
import org.matrix.rustcomponents.sdk.NotificationClient
|
||||||
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
||||||
@@ -70,6 +71,7 @@ class FakeFfiClient(
|
|||||||
private val isProfilesSlidingSyncExtensionSupportedResult: () -> Boolean = { false },
|
private val isProfilesSlidingSyncExtensionSupportedResult: () -> Boolean = { false },
|
||||||
private val subscribeToOwnProfileResult: (ProfileListener) -> Unit = {},
|
private val subscribeToOwnProfileResult: (ProfileListener) -> Unit = {},
|
||||||
private val getUrlResult: (String) -> ByteArray = { lambdaError() },
|
private val getUrlResult: (String) -> ByteArray = { lambdaError() },
|
||||||
|
private val getMediaThumbnailResult: (MediaSource, ULong, ULong) -> ByteArray = { _, _, _ -> byteArrayOf() },
|
||||||
private val contentScannerResult: () -> ContentScanner = { FakeFfiContentScanner() },
|
private val contentScannerResult: () -> ContentScanner = { FakeFfiContentScanner() },
|
||||||
private val closeResult: () -> Unit = {},
|
private val closeResult: () -> Unit = {},
|
||||||
) : Client(NoHandle) {
|
) : Client(NoHandle) {
|
||||||
@@ -122,6 +124,10 @@ class FakeFfiClient(
|
|||||||
return getProfileResult(userId)
|
return getProfileResult(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMediaThumbnail(mediaSource: MediaSource, width: ULong, height: ULong): ByteArray {
|
||||||
|
return getMediaThumbnailResult(mediaSource, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun homeserverLoginDetails(): HomeserverLoginDetails {
|
override suspend fun homeserverLoginDetails(): HomeserverLoginDetails {
|
||||||
return homeserverLoginDetailsResult()
|
return homeserverLoginDetailsResult()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -20,6 +20,7 @@ fun MatrixUser.getAvatarData(size: AvatarSize) = AvatarData(
|
|||||||
name = displayName,
|
name = displayName,
|
||||||
url = avatarUrl,
|
url = avatarUrl,
|
||||||
size = size,
|
size = size,
|
||||||
|
thumbnail = avatarThumbnail,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun MatrixUser.getBestName(): String {
|
fun MatrixUser.getBestName(): String {
|
||||||
|
|||||||
+2
@@ -33,12 +33,14 @@ class MatrixUserExtensionsTest : RobolectricTest() {
|
|||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
displayName = "displayName",
|
displayName = "displayName",
|
||||||
avatarUrl = "avatarUrl",
|
avatarUrl = "avatarUrl",
|
||||||
|
avatarThumbnail = "avatarThumbnail",
|
||||||
)
|
)
|
||||||
val expected = AvatarData(
|
val expected = AvatarData(
|
||||||
id = A_USER_ID.value,
|
id = A_USER_ID.value,
|
||||||
name = "displayName",
|
name = "displayName",
|
||||||
url = "avatarUrl",
|
url = "avatarUrl",
|
||||||
size = AvatarSize.UserHeader,
|
size = AvatarSize.UserHeader,
|
||||||
|
thumbnail = "avatarThumbnail",
|
||||||
)
|
)
|
||||||
assertThat(matrixUser.getAvatarData(AvatarSize.UserHeader)).isEqualTo(expected)
|
assertThat(matrixUser.getAvatarData(AvatarSize.UserHeader)).isEqualTo(expected)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -46,4 +46,6 @@ data class SessionData(
|
|||||||
val userDisplayName: String?,
|
val userDisplayName: String?,
|
||||||
/** The optional avatar URL of the user. */
|
/** The optional avatar URL of the user. */
|
||||||
val userAvatarUrl: String?,
|
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 sessionId the session to update.
|
||||||
* @param displayName the new display name, or `null` when the user has none.
|
* @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 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.
|
* Get the session data matching the userId, or null if not found.
|
||||||
|
|||||||
+3
-1
@@ -84,12 +84,13 @@ class DatabaseSessionStore(
|
|||||||
lastUsageIndex = result.lastUsageIndex,
|
lastUsageIndex = result.lastUsageIndex,
|
||||||
userDisplayName = result.userDisplayName,
|
userDisplayName = result.userDisplayName,
|
||||||
userAvatarUrl = result.userAvatarUrl,
|
userAvatarUrl = result.userAvatarUrl,
|
||||||
|
userAvatarData = result.userAvatarData,
|
||||||
).toDbModel()
|
).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 {
|
sessionDataMutex.withLock {
|
||||||
val result = database.sessionDataQueries.selectByUserId(sessionId)
|
val result = database.sessionDataQueries.selectByUserId(sessionId)
|
||||||
.executeAsOneOrNull()
|
.executeAsOneOrNull()
|
||||||
@@ -102,6 +103,7 @@ class DatabaseSessionStore(
|
|||||||
result.copy(
|
result.copy(
|
||||||
userDisplayName = displayName,
|
userDisplayName = displayName,
|
||||||
userAvatarUrl = avatarUrl,
|
userAvatarUrl = avatarUrl,
|
||||||
|
userAvatarData = avatarData,
|
||||||
).toDbModel()
|
).toDbModel()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -31,6 +31,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
|||||||
lastUsageIndex = lastUsageIndex,
|
lastUsageIndex = lastUsageIndex,
|
||||||
userDisplayName = userDisplayName,
|
userDisplayName = userDisplayName,
|
||||||
userAvatarUrl = userAvatarUrl,
|
userAvatarUrl = userAvatarUrl,
|
||||||
|
userAvatarData = userAvatarData,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,5 +53,6 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
|||||||
lastUsageIndex = lastUsageIndex,
|
lastUsageIndex = lastUsageIndex,
|
||||||
userDisplayName = userDisplayName,
|
userDisplayName = userDisplayName,
|
||||||
userAvatarUrl = userAvatarUrl,
|
userAvatarUrl = userAvatarUrl,
|
||||||
|
userAvatarData = userAvatarData,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
+4
-1
@@ -36,7 +36,10 @@ CREATE TABLE SessionData (
|
|||||||
-- user display name
|
-- user display name
|
||||||
userDisplayName TEXT,
|
userDisplayName TEXT,
|
||||||
-- user avatar url
|
-- 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
|
@Test
|
||||||
fun `updateUserProfile does nothing if the session is not found`() = runTest {
|
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()
|
assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `updateUserProfile update the data`() = runTest {
|
fun `updateUserProfile update the data`() = runTest {
|
||||||
database.sessionDataQueries.insertSessionData(aSessionData)
|
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()
|
val updatedSession = database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne()
|
||||||
assertThat(updatedSession.userDisplayName).isEqualTo("userDisplayName")
|
assertThat(updatedSession.userDisplayName).isEqualTo("userDisplayName")
|
||||||
assertThat(updatedSession.userAvatarUrl).isEqualTo("userAvatarUrl")
|
assertThat(updatedSession.userAvatarUrl).isEqualTo("userAvatarUrl")
|
||||||
|
assertThat(updatedSession.userAvatarData).isEqualTo("userAvatarData")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -231,6 +232,7 @@ class DatabaseSessionStoreTest {
|
|||||||
lastUsageIndex = 0,
|
lastUsageIndex = 0,
|
||||||
userDisplayName = "userDisplayName",
|
userDisplayName = "userDisplayName",
|
||||||
userAvatarUrl = "userAvatarUrl",
|
userAvatarUrl = "userAvatarUrl",
|
||||||
|
userAvatarData = "userAvatarData",
|
||||||
)
|
)
|
||||||
val secondSessionData = SessionData(
|
val secondSessionData = SessionData(
|
||||||
userId = "userId",
|
userId = "userId",
|
||||||
@@ -249,6 +251,7 @@ class DatabaseSessionStoreTest {
|
|||||||
lastUsageIndex = 1,
|
lastUsageIndex = 1,
|
||||||
userDisplayName = "userDisplayNameAltered",
|
userDisplayName = "userDisplayNameAltered",
|
||||||
userAvatarUrl = "userAvatarUrlAltered",
|
userAvatarUrl = "userAvatarUrlAltered",
|
||||||
|
userAvatarData = "userAvatarDataAltered",
|
||||||
)
|
)
|
||||||
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
||||||
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
||||||
@@ -273,6 +276,7 @@ class DatabaseSessionStoreTest {
|
|||||||
assertThat(alteredSession.lastUsageIndex).isEqualTo(firstSessionData.lastUsageIndex)
|
assertThat(alteredSession.lastUsageIndex).isEqualTo(firstSessionData.lastUsageIndex)
|
||||||
assertThat(alteredSession.userDisplayName).isEqualTo(firstSessionData.userDisplayName)
|
assertThat(alteredSession.userDisplayName).isEqualTo(firstSessionData.userDisplayName)
|
||||||
assertThat(alteredSession.userAvatarUrl).isEqualTo(firstSessionData.userAvatarUrl)
|
assertThat(alteredSession.userAvatarUrl).isEqualTo(firstSessionData.userAvatarUrl)
|
||||||
|
assertThat(alteredSession.userAvatarData).isEqualTo(firstSessionData.userAvatarData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -294,6 +298,7 @@ class DatabaseSessionStoreTest {
|
|||||||
lastUsageIndex = 0,
|
lastUsageIndex = 0,
|
||||||
userDisplayName = "userDisplayName",
|
userDisplayName = "userDisplayName",
|
||||||
userAvatarUrl = "userAvatarUrl",
|
userAvatarUrl = "userAvatarUrl",
|
||||||
|
userAvatarData = "userAvatarData",
|
||||||
)
|
)
|
||||||
val secondSessionData = SessionData(
|
val secondSessionData = SessionData(
|
||||||
userId = "userIdUnknown",
|
userId = "userIdUnknown",
|
||||||
@@ -312,6 +317,7 @@ class DatabaseSessionStoreTest {
|
|||||||
lastUsageIndex = 1,
|
lastUsageIndex = 1,
|
||||||
userDisplayName = "userDisplayNameAltered",
|
userDisplayName = "userDisplayNameAltered",
|
||||||
userAvatarUrl = "userAvatarUrlAltered",
|
userAvatarUrl = "userAvatarUrlAltered",
|
||||||
|
userAvatarData = "userAvatarDataAltered",
|
||||||
)
|
)
|
||||||
assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId)
|
assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId)
|
||||||
|
|
||||||
|
|||||||
+1
@@ -30,4 +30,5 @@ internal fun aDbSessionData(
|
|||||||
lastUsageIndex = 0,
|
lastUsageIndex = 0,
|
||||||
userDisplayName = null,
|
userDisplayName = null,
|
||||||
userAvatarUrl = null,
|
userAvatarUrl = null,
|
||||||
|
userAvatarData = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
|
|
||||||
class InMemorySessionStore(
|
class InMemorySessionStore(
|
||||||
initialList: List<SessionData> = emptyList(),
|
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") },
|
private val setLatestSessionResult: (String) -> Unit = { error("Not implemented") },
|
||||||
) : SessionStore {
|
) : SessionStore {
|
||||||
private val sessionDataListFlow = MutableStateFlow(initialList)
|
private val sessionDataListFlow = MutableStateFlow(initialList)
|
||||||
@@ -56,8 +56,8 @@ class InMemorySessionStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) {
|
override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?, avatarData: String?) {
|
||||||
updateUserProfileResult(sessionId, displayName, avatarUrl)
|
updateUserProfileResult(sessionId, displayName, avatarUrl, avatarData)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSession(sessionId: String): SessionData? {
|
override suspend fun getSession(sessionId: String): SessionData? {
|
||||||
|
|||||||
+2
@@ -23,6 +23,7 @@ fun aSessionData(
|
|||||||
lastUsageIndex: Long = 0,
|
lastUsageIndex: Long = 0,
|
||||||
userDisplayName: String? = null,
|
userDisplayName: String? = null,
|
||||||
userAvatarUrl: String? = null,
|
userAvatarUrl: String? = null,
|
||||||
|
userAvatarData: String? = null,
|
||||||
): SessionData {
|
): SessionData {
|
||||||
return SessionData(
|
return SessionData(
|
||||||
userId = sessionId,
|
userId = sessionId,
|
||||||
@@ -41,5 +42,6 @@ fun aSessionData(
|
|||||||
lastUsageIndex = lastUsageIndex,
|
lastUsageIndex = lastUsageIndex,
|
||||||
userDisplayName = userDisplayName,
|
userDisplayName = userDisplayName,
|
||||||
userAvatarUrl = userAvatarUrl,
|
userAvatarUrl = userAvatarUrl,
|
||||||
|
userAvatarData = userAvatarData,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user