Take into account homeserver capabilities (#6507)

* Take into account homeserver capabilities: add `HomeserverCapabilitiesProvider` to check if the HS allows changing the user's display name or avatar. Also, modify the edit user profile screen to reflect these values.

* Add `/myavatar` command. Filter both `/nick` and `/myavatar` commands based on the homeserver capabilities.

* Update screenshots

* Assume the use can change their display name and avatar url if the capabilities check fails: if they try to change those, the HS will return an error anyway.

* Disable also `/myroomname` and `/myroomavatar` based on the HS capabilities.

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2026-04-15 14:29:41 +02:00 committed by GitHub
parent 80470b3792
commit 66513bc905
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 363 additions and 14 deletions

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
class RustHomeserverCapabilitiesProvider(
private val homeserverCapabilities: HomeserverCapabilities,
) : HomeserverCapabilitiesProvider {
override suspend fun refresh(): Result<Unit> = runCatchingExceptions {
homeserverCapabilities.refresh()
}
override suspend fun canChangeDisplayName(): Result<Boolean> = runCatchingExceptions {
homeserverCapabilities.canChangeDisplayname()
}
override suspend fun canChangeAvatarUrl(): Result<Boolean> = runCatchingExceptions {
homeserverCapabilities.canChangeAvatar()
}
}

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
import io.element.android.libraries.matrix.api.core.DeviceId
@ -835,6 +836,10 @@ class RustMatrixClient(
val request = PerformDatabaseVacuumRequestBuilder(sessionId)
sessionCoroutineScope.launch { workManagerScheduler.submit(request) }
}
override fun homeserverCapabilities(): HomeserverCapabilitiesProvider {
return RustHomeserverCapabilitiesProvider(innerClient.homeserverCapabilities())
}
}
private fun defaultRoomCreationPowerLevels(isPublic: Boolean, isSpace: Boolean) = PowerLevels(

View file

@ -13,6 +13,7 @@ import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
@ -90,4 +91,9 @@ object SessionMatrixModule {
fun providesSpaceService(matrixClient: MatrixClient): SpaceService {
return matrixClient.spaceService
}
@Provides
fun providesHomeserverCapabilitiesProvider(matrixClient: MatrixClient): HomeserverCapabilitiesProvider {
return matrixClient.homeserverCapabilities()
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverCapabilities
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.runTest
import org.junit.Test
class RustHomeserverCapabilitiesProviderTest {
@Test
fun `refresh calls client refresh`() = runTest {
val refreshLambda = lambdaRecorder<Unit> {}
val provider = createCapabilitiesProvider(
capabilities = FakeFfiHomeserverCapabilities(refresh = refreshLambda),
)
assertThat(provider.refresh().isSuccess).isTrue()
refreshLambda.assertions().isCalledOnce()
}
@Test
fun `refresh fails when client refresh does`() = runTest {
val refreshLambda = lambdaRecorder<Unit> { throw IllegalStateException("Failed to refresh capabilities") }
val provider = createCapabilitiesProvider(
capabilities = FakeFfiHomeserverCapabilities(refresh = refreshLambda),
)
assertThat(provider.refresh().isFailure).isTrue()
refreshLambda.assertions().isCalledOnce()
}
@Test
fun `canChangeDisplayName returns expected value`() = runTest {
val provider = createCapabilitiesProvider(
capabilities = FakeFfiHomeserverCapabilities(canChangeDisplayName = { true }),
)
assertThat(provider.canChangeDisplayName().getOrNull()).isTrue()
}
@Test
fun `canChangeAvatarUrl returns expected value`() = runTest {
val provider = createCapabilitiesProvider(
capabilities = FakeFfiHomeserverCapabilities(canChangeAvatar = { true }),
)
assertThat(provider.canChangeAvatarUrl().getOrNull()).isTrue()
}
@Test
fun `canChangeDisplayName returns failure when client throws`() = runTest {
val provider = createCapabilitiesProvider(
capabilities = FakeFfiHomeserverCapabilities(canChangeDisplayName = { throw IllegalStateException("Failed to get display name capability") }),
)
assert(provider.canChangeDisplayName().isFailure)
}
private fun createCapabilitiesProvider(
capabilities: FakeFfiHomeserverCapabilities = FakeFfiHomeserverCapabilities(),
) = RustHomeserverCapabilitiesProvider(capabilities)
}

View file

@ -17,6 +17,7 @@ import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.CreateRoomParameters
import org.matrix.rustcomponents.sdk.Encryption
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
import org.matrix.rustcomponents.sdk.NoHandle
@ -50,6 +51,7 @@ class FakeFfiClient(
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
private val getStoreSizesResult: () -> StoreSizes = { lambdaError() },
private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() },
private val homeserverCapabilities: HomeserverCapabilities = FakeFfiHomeserverCapabilities(),
private val closeResult: () -> Unit = {},
) : Client(NoHandle) {
override fun userId(): String = userId
@ -103,5 +105,9 @@ class FakeFfiClient(
return createRoomResult(request)
}
override fun homeserverCapabilities(): HomeserverCapabilities {
return homeserverCapabilities
}
override fun close() = closeResult()
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.tests.testutils.lambda.lambdaError
import org.matrix.rustcomponents.sdk.ExtendedProfileFields
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
import org.matrix.rustcomponents.sdk.NoHandle
class FakeFfiHomeserverCapabilities(
private val refresh: () -> Unit = { lambdaError() },
private val canChangeDisplayName: () -> Boolean = { lambdaError() },
private val canChangeAvatar: () -> Boolean = { lambdaError() },
private val canChangePassword: () -> Boolean = { lambdaError() },
private val canChangeThirdPartyIds: () -> Boolean = { lambdaError() },
private val canGetLoginToken: () -> Boolean = { lambdaError() },
private val forgetsRoomWhenLeaving: () -> Boolean = { lambdaError() },
private val extendedProfileFields: () -> ExtendedProfileFields = { lambdaError() },
) : HomeserverCapabilities(NoHandle) {
override suspend fun refresh() = refresh.invoke()
override suspend fun canChangeDisplayname(): Boolean = canChangeDisplayName.invoke()
override suspend fun canChangeAvatar(): Boolean = canChangeAvatar.invoke()
override suspend fun canChangePassword(): Boolean = canChangePassword.invoke()
override suspend fun canChangeThirdpartyIds(): Boolean = canChangeThirdPartyIds.invoke()
override suspend fun canGetLoginToken(): Boolean = canGetLoginToken.invoke()
override suspend fun forgetsRoomWhenLeaving(): Boolean = forgetsRoomWhenLeaving.invoke()
override suspend fun extendedProfileFields(): ExtendedProfileFields = extendedProfileFields.invoke()
}