From 682897cdb6a9a84ca05633633ae2ccf383889b79 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Jun 2025 20:38:39 +0200 Subject: [PATCH] change (media preview config) : introduce new apis from sdk --- .../libraries/matrix/api/MatrixClient.kt | 7 ++ .../matrix/api/media/MediaPreviewConfig.kt | 16 ++++ .../matrix/api/media/MediaPreviewService.kt | 42 ++++++++++ .../libraries/matrix/impl/RustMatrixClient.kt | 14 ++++ .../matrix/impl/di/SessionMatrixModule.kt | 6 ++ .../impl/media/RustMediaPreviewService.kt | 84 +++++++++++++++++++ .../libraries/matrix/test/FakeMatrixClient.kt | 8 ++ .../test/media/FakeMediaPreviewService.kt | 50 +++++++++++ 8 files changed, 227 insertions(+) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index f7d95d7078..81aa28be6b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.api import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.ProgressCallback @@ -19,6 +20,9 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewService +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction @@ -40,6 +44,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.withContext import java.util.Optional interface MatrixClient { @@ -72,6 +77,7 @@ interface MatrixClient { fun notificationSettingsService(): NotificationSettingsService fun encryptionService(): EncryptionService fun roomDirectoryService(): RoomDirectoryService + fun mediaPreviewService(): MediaPreviewService suspend fun getCacheSize(): Long /** @@ -169,6 +175,7 @@ interface MatrixClient { * Return true if Livekit Rtc is supported, i.e. if Element Call is available. */ suspend fun isLivekitRtcSupported(): Boolean + } /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt new file mode 100644 index 0000000000..5c964ca15e --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.media + +/** + * Configuration for media preview ie. invite avatars and timeline media. + */ +data class MediaPreviewConfig( + val mediaPreviewValue: MediaPreviewValue, + val hideInviteAvatar: Boolean, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt new file mode 100644 index 0000000000..19cbe9cf2a --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.media + +import kotlinx.coroutines.flow.Flow + +interface MediaPreviewService { + /** + * Will fetch the media preview config from the server. + */ + suspend fun fetchMediaPreviewConfig(): Result + + /** + * Will emit the media preview config known by the client. + * This will emit a new value when received from sync. + */ + fun getMediaPreviewConfigFlow(): Flow + + /** + * Get the media preview display policy from the cache. This value is updated through sync. + */ + suspend fun getMediaPreviewValue(): MediaPreviewValue? + + /** + * Get the invite avatars display policy from the cache. This value is updated through sync. + */ + suspend fun getHideInviteAvatars(): Boolean + + /** + * Set the media preview display policy. This will update the value on the server and update the local value when successful. + */ + suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result + /** + * Set the invite avatars display policy. This will update the value on the server and update the local value when successful. + */ + suspend fun setHideInviteAvatars(hide: Boolean): Result +} 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 07b8b3de09..1e9d703b28 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 @@ -26,6 +26,9 @@ import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewService +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction @@ -52,6 +55,7 @@ import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService import io.element.android.libraries.matrix.impl.exception.mapClientException import io.element.android.libraries.matrix.impl.media.RustMediaLoader +import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.oidc.toRustAction @@ -107,6 +111,7 @@ import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientException import org.matrix.rustcomponents.sdk.IgnoredUsersListener +import org.matrix.rustcomponents.sdk.InviteAvatars import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.PowerLevels import org.matrix.rustcomponents.sdk.RoomInfoListener @@ -214,6 +219,11 @@ class RustMatrixClient( innerClient = innerClient, ) + private val mediaPreviewService = RustMediaPreviewService( + innerClient = innerClient, + sessionDispatcher = sessionDispatcher, + ) + private var clientDelegateTaskHandle: TaskHandle? = innerClient.setDelegate(sessionDelegate) private val _userProfile: MutableStateFlow = MutableStateFlow( @@ -507,6 +517,8 @@ class RustMatrixClient( override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService + override fun mediaPreviewService(): MediaPreviewService = mediaPreviewService + internal suspend fun destroy() { innerNotificationClient.close() @@ -682,6 +694,8 @@ class RustMatrixClient( innerClient.isLivekitRtcSupported() } + + private suspend fun File.getCacheSize( includeCryptoDb: Boolean = false, ): Long = withContext(sessionDispatcher) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 361d1efd97..6a04079a7e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService @@ -71,4 +72,9 @@ object SessionMatrixModule { fun providesRoomDirectoryService(matrixClient: MatrixClient): RoomDirectoryService { return matrixClient.roomDirectoryService() } + + @Provides + fun providesMediaPreviewService(matrixClient: MatrixClient): MediaPreviewService { + return matrixClient.mediaPreviewService() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt new file mode 100644 index 0000000000..a2ab6c8bb9 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.media + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewService +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.impl.util.mxCallbackFlow +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.InviteAvatars +import org.matrix.rustcomponents.sdk.MediaPreviewConfigListener +import org.matrix.rustcomponents.sdk.MediaPreviews +import org.matrix.rustcomponents.sdk.MediaPreviewConfig as RustMediaPreviewConfig + +class RustMediaPreviewService( + private val sessionDispatcher: CoroutineDispatcher, + private val innerClient: Client, +) : MediaPreviewService { + override suspend fun fetchMediaPreviewConfig(): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.fetchMediaPreviewConfig()?.into() + } + } + + override fun getMediaPreviewConfigFlow(): Flow = innerClient.getMediaPreviewConfigFlow() + + override suspend fun getMediaPreviewValue(): MediaPreviewValue? = innerClient.getMediaPreviewDisplayPolicy()?.into() + + override suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.setMediaPreviewDisplayPolicy(mediaPreviewValue.into()) + } + } + + override suspend fun getHideInviteAvatars(): Boolean = innerClient.getInviteAvatarsDisplayPolicy() == InviteAvatars.OFF + + override suspend fun setHideInviteAvatars(hide: Boolean): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + val inviteAvatars = if (hide) InviteAvatars.OFF else InviteAvatars.ON + innerClient.setInviteAvatarsDisplayPolicy(inviteAvatars) + } + } +} + +private fun RustMediaPreviewConfig.into(): MediaPreviewConfig { + return MediaPreviewConfig( + mediaPreviewValue = this@into.mediaPreviews.into(), + hideInviteAvatar = inviteAvatars == InviteAvatars.OFF + ) +} + +private fun Client.getMediaPreviewConfigFlow() = mxCallbackFlow { + subscribeToMediaPreviewConfig(object : MediaPreviewConfigListener { + override fun onChange(mediaPreviewConfig: RustMediaPreviewConfig?) { + trySend(mediaPreviewConfig?.into()) + } + }) +} + +private fun MediaPreviewValue.into(): MediaPreviews { + return when (this) { + MediaPreviewValue.On -> MediaPreviews.ON + MediaPreviewValue.Off -> MediaPreviews.OFF + MediaPreviewValue.Private -> MediaPreviews.PRIVATE + } +} + +private fun MediaPreviews.into(): MediaPreviewValue { + return when (this) { + MediaPreviews.ON -> MediaPreviewValue.On + MediaPreviews.OFF -> MediaPreviewValue.Off + MediaPreviews.PRIVATE -> MediaPreviewValue.Private + } +} + diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index c76d54a85c..11b95a647e 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -17,7 +17,10 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction @@ -36,6 +39,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader +import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.pushers.FakePushersService @@ -72,6 +76,7 @@ class FakeMatrixClient( private val syncService: FakeSyncService = FakeSyncService(), private val encryptionService: FakeEncryptionService = FakeEncryptionService(), private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(), + private val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(), private val accountManagementUrlResult: (AccountManagementAction?) -> Result = { lambdaError() }, private val resolveRoomAliasResult: (RoomAlias) -> Result> = { Result.success( @@ -91,6 +96,7 @@ class FakeMatrixClient( private val canReportRoomLambda: () -> Boolean = { false }, private val isLivekitRtcSupportedLambda: () -> Boolean = { false }, override val ignoredUsersFlow: StateFlow> = MutableStateFlow(persistentListOf()), + ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -234,11 +240,13 @@ class FakeMatrixClient( override fun notificationService(): NotificationService = notificationService override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService override fun encryptionService(): EncryptionService = encryptionService + override fun mediaPreviewService(): MediaPreviewService = mediaPreviewService override fun roomMembershipObserver(): RoomMembershipObserver { return RoomMembershipObserver() } + // Mocks fun givenCreateRoomResult(result: Result) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt new file mode 100644 index 0000000000..7e41b03e41 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.test.media + +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewService +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class FakeMediaPreviewService( + private val fetchMediaPreviewConfigResult: () -> Result = { lambdaError() }, + private val mediaPreviewConfigFlow: Flow = flowOf(null), + private val getMediaPreviewValue: ()-> MediaPreviewValue? = { null }, + private val getHideInviteAvatars: () -> Boolean = { false }, + private val setMediaPreviewValueResult: (MediaPreviewValue) -> Result = { lambdaError() }, + private val setHideInviteAvatarsResult: (Boolean) -> Result = { lambdaError() }, +): MediaPreviewService { + + override suspend fun fetchMediaPreviewConfig(): Result = simulateLongTask { + fetchMediaPreviewConfigResult() + } + + override fun getMediaPreviewConfigFlow(): Flow { + return mediaPreviewConfigFlow + } + + override suspend fun getMediaPreviewValue(): MediaPreviewValue? = simulateLongTask { + getMediaPreviewValue.invoke() + } + + override suspend fun getHideInviteAvatars(): Boolean = simulateLongTask { + getHideInviteAvatars.invoke() + } + + override suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result = simulateLongTask { + setMediaPreviewValueResult(mediaPreviewValue) + } + + override suspend fun setHideInviteAvatars(hide: Boolean): Result = simulateLongTask { + setHideInviteAvatarsResult(hide) + } +}