Skip to content

Commit 66513bc

Browse files
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>
1 parent 80470b3 commit 66513bc

File tree

26 files changed

+363
-14
lines changed

26 files changed

+363
-14
lines changed

appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import androidx.compose.runtime.setValue
2121
import dev.zacsweers.metro.Inject
2222
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
2323
import im.vector.app.features.analytics.plan.UserProperties
24+
import io.element.android.features.networkmonitor.api.NetworkMonitor
25+
import io.element.android.features.networkmonitor.api.NetworkStatus
2426
import io.element.android.libraries.architecture.AsyncData
2527
import io.element.android.libraries.architecture.Presenter
2628
import io.element.android.libraries.core.extensions.runCatchingExceptions
@@ -56,6 +58,7 @@ class LoggedInPresenter(
5658
private val analyticsService: AnalyticsService,
5759
private val encryptionService: EncryptionService,
5860
private val buildMeta: BuildMeta,
61+
private val networkMonitor: NetworkMonitor,
5962
) : Presenter<LoggedInState> {
6063
@Composable
6164
override fun present(): LoggedInState {
@@ -107,6 +110,14 @@ class LoggedInPresenter(
107110
}.launchIn(this)
108111
}
109112

113+
val networkConnectivity by networkMonitor.connectivity.collectAsState()
114+
LaunchedEffect(networkConnectivity) {
115+
if (networkConnectivity == NetworkStatus.Connected) {
116+
// Refresh homeserver capabilities when the network is back
117+
matrixClient.homeserverCapabilities().refresh()
118+
}
119+
}
120+
110121
fun handleEvent(event: LoggedInEvents) {
111122
when (event) {
112123
is LoggedInEvents.CloseErrorDialog -> {

appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import app.cash.turbine.ReceiveTurbine
1414
import com.google.common.truth.Truth.assertThat
1515
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
1616
import im.vector.app.features.analytics.plan.UserProperties
17+
import io.element.android.features.networkmonitor.api.NetworkStatus
18+
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
1719
import io.element.android.libraries.core.meta.BuildMeta
1820
import io.element.android.libraries.matrix.api.MatrixClient
1921
import io.element.android.libraries.matrix.api.core.SessionId
@@ -27,6 +29,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
2729
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
2830
import io.element.android.libraries.matrix.test.AN_EXCEPTION
2931
import io.element.android.libraries.matrix.test.A_SESSION_ID
32+
import io.element.android.libraries.matrix.test.FakeHomeserverCapabilitiesProvider
3033
import io.element.android.libraries.matrix.test.FakeMatrixClient
3134
import io.element.android.libraries.matrix.test.core.aBuildMeta
3235
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
@@ -109,6 +112,7 @@ class LoggedInPresenterTest {
109112
val verificationService = FakeSessionVerificationService()
110113
val encryptionService = FakeEncryptionService()
111114
val buildMeta = aBuildMeta()
115+
val networkMonitor = FakeNetworkMonitor()
112116
LoggedInPresenter(
113117
matrixClient = FakeMatrixClient(
114118
roomListService = roomListService,
@@ -122,6 +126,7 @@ class LoggedInPresenterTest {
122126
analyticsService = analyticsService,
123127
encryptionService = encryptionService,
124128
buildMeta = buildMeta,
129+
networkMonitor = networkMonitor,
125130
).test {
126131
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
127132
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
@@ -319,6 +324,27 @@ class LoggedInPresenterTest {
319324
}
320325
}
321326

327+
@Test
328+
fun `present - refreshes homeserver capabilities when network is back`() = runTest {
329+
val refreshLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
330+
val matrixClient = FakeMatrixClient(
331+
homeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(refresh = refreshLambda),
332+
accountManagementUrlResult = { Result.success(null) },
333+
)
334+
val networkMonitor = FakeNetworkMonitor()
335+
createLoggedInPresenter(
336+
matrixClient = matrixClient,
337+
networkMonitor = networkMonitor,
338+
).test {
339+
awaitItem()
340+
networkMonitor.connectivity.value = NetworkStatus.Connected
341+
342+
advanceUntilIdle()
343+
344+
refreshLambda.assertions().isCalledOnce()
345+
}
346+
}
347+
322348
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
323349
skipItems(1)
324350
return awaitItem()
@@ -334,6 +360,7 @@ class LoggedInPresenterTest {
334360
accountManagementUrlResult = { Result.success(null) },
335361
),
336362
buildMeta: BuildMeta = aBuildMeta(),
363+
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
337364
): LoggedInPresenter {
338365
return LoggedInPresenter(
339366
matrixClient = matrixClient,
@@ -343,6 +370,7 @@ class LoggedInPresenterTest {
343370
analyticsService = analyticsService,
344371
encryptionService = encryptionService,
345372
buildMeta = buildMeta,
373+
networkMonitor = networkMonitor,
346374
)
347375
}
348376
}

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.compose.runtime.MutableState
1515
import androidx.compose.runtime.derivedStateOf
1616
import androidx.compose.runtime.getValue
1717
import androidx.compose.runtime.mutableStateOf
18+
import androidx.compose.runtime.produceState
1819
import androidx.compose.runtime.remember
1920
import androidx.compose.runtime.rememberCoroutineScope
2021
import androidx.compose.runtime.saveable.rememberSaveable
@@ -103,6 +104,14 @@ class EditUserProfilePresenter(
103104
}
104105
}
105106

107+
val homeserverCapabilities = matrixClient.homeserverCapabilities()
108+
val canChangeDisplayName = produceState(true) {
109+
value = homeserverCapabilities.canChangeDisplayName().getOrDefault(true)
110+
}
111+
val canChangeAvatar = produceState(true) {
112+
value = homeserverCapabilities.canChangeAvatarUrl().getOrDefault(true)
113+
}
114+
106115
val saveAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
107116
val localCoroutineScope = rememberCoroutineScope()
108117

@@ -169,6 +178,8 @@ class EditUserProfilePresenter(
169178
saveButtonEnabled = canSave && saveAction.value !is AsyncAction.Loading,
170179
saveAction = saveAction.value,
171180
cameraPermissionState = cameraPermissionState,
181+
canChangeDisplayName = canChangeDisplayName.value,
182+
canChangeAvatarUrl = canChangeAvatar.value,
172183
eventSink = ::handleEvent,
173184
)
174185
}

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ data class EditUserProfileState(
2222
val saveButtonEnabled: Boolean,
2323
val saveAction: AsyncAction<Unit>,
2424
val cameraPermissionState: PermissionsState,
25+
val canChangeDisplayName: Boolean,
26+
val canChangeAvatarUrl: Boolean,
2527
val eventSink: (EditUserProfileEvent) -> Unit
2628
)

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ open class EditUserProfileStateProvider : PreviewParameterProvider<EditUserProfi
2222
aEditUserProfileState(),
2323
aEditUserProfileState(userAvatarUrl = "example://uri"),
2424
aEditUserProfileState(saveAction = AsyncAction.ConfirmingCancellation),
25+
aEditUserProfileState(canChangeAvatarUrl = false, canChangeDisplayName = false),
2526
)
2627
}
2728

@@ -33,6 +34,8 @@ fun aEditUserProfileState(
3334
saveButtonEnabled: Boolean = true,
3435
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
3536
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
37+
canChangeDisplayName: Boolean = true,
38+
canChangeAvatarUrl: Boolean = true,
3639
eventSink: (EditUserProfileEvent) -> Unit = {},
3740
) = EditUserProfileState(
3841
userId = userId,
@@ -42,5 +45,7 @@ fun aEditUserProfileState(
4245
saveButtonEnabled = saveButtonEnabled,
4346
saveAction = saveAction,
4447
cameraPermissionState = cameraPermissionState,
48+
canChangeDisplayName = canChangeDisplayName,
49+
canChangeAvatarUrl = canChangeAvatarUrl,
4550
eventSink = eventSink,
4651
)

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ fun EditUserProfileView(
120120
state = avatarPickerState,
121121
onClick = ::onAvatarClick,
122122
modifier = Modifier.align(Alignment.CenterHorizontally),
123+
enabled = state.canChangeAvatarUrl,
123124
)
124125
Spacer(modifier = Modifier.height(16.dp))
125126
Text(
@@ -134,6 +135,7 @@ fun EditUserProfileView(
134135
value = state.displayName,
135136
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
136137
singleLine = true,
138+
enabled = state.canChangeDisplayName,
137139
onValueChange = { state.eventSink(EditUserProfileEvent.UpdateDisplayName(it)) },
138140
)
139141
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.api
9+
10+
/**
11+
* Provides information about the capabilities of the homeserver.
12+
*
13+
* Spec: https://spec.matrix.org/latest/client-server-api/#capabilities-negotiation
14+
*/
15+
interface HomeserverCapabilitiesProvider {
16+
/**
17+
* Manually refresh the capabilities of the homeserver performing a network request.
18+
*/
19+
suspend fun refresh(): Result<Unit>
20+
21+
/**
22+
* Indicates whether the homeserver allows the user to change their display name.
23+
*/
24+
suspend fun canChangeDisplayName(): Result<Boolean>
25+
26+
/**
27+
* Indicates whether the homeserver allows the user to change their avatar URL.
28+
*/
29+
suspend fun canChangeAvatarUrl(): Result<Boolean>
30+
}

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ interface MatrixClient {
223223
* Resets the cached client `well-known` config by the SDK.
224224
*/
225225
suspend fun resetWellKnownConfig(): Result<Unit>
226+
227+
fun homeserverCapabilities(): HomeserverCapabilitiesProvider
226228
}
227229

228230
/**
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.impl
9+
10+
import io.element.android.libraries.core.extensions.runCatchingExceptions
11+
import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider
12+
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
13+
14+
class RustHomeserverCapabilitiesProvider(
15+
private val homeserverCapabilities: HomeserverCapabilities,
16+
) : HomeserverCapabilitiesProvider {
17+
override suspend fun refresh(): Result<Unit> = runCatchingExceptions {
18+
homeserverCapabilities.refresh()
19+
}
20+
21+
override suspend fun canChangeDisplayName(): Result<Boolean> = runCatchingExceptions {
22+
homeserverCapabilities.canChangeDisplayname()
23+
}
24+
25+
override suspend fun canChangeAvatarUrl(): Result<Boolean> = runCatchingExceptions {
26+
homeserverCapabilities.canChangeAvatar()
27+
}
28+
}

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import io.element.android.libraries.core.data.tryOrNull
1717
import io.element.android.libraries.core.extensions.mapFailure
1818
import io.element.android.libraries.core.extensions.runCatchingExceptions
1919
import io.element.android.libraries.featureflag.api.FeatureFlagService
20+
import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider
2021
import io.element.android.libraries.matrix.api.MatrixClient
2122
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
2223
import io.element.android.libraries.matrix.api.core.DeviceId
@@ -835,6 +836,10 @@ class RustMatrixClient(
835836
val request = PerformDatabaseVacuumRequestBuilder(sessionId)
836837
sessionCoroutineScope.launch { workManagerScheduler.submit(request) }
837838
}
839+
840+
override fun homeserverCapabilities(): HomeserverCapabilitiesProvider {
841+
return RustHomeserverCapabilitiesProvider(innerClient.homeserverCapabilities())
842+
}
838843
}
839844

840845
private fun defaultRoomCreationPowerLevels(isPublic: Boolean, isSpace: Boolean) = PowerLevels(

0 commit comments

Comments
 (0)