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 15419bb675
commit b66df37f3e
26 changed files with 363 additions and 14 deletions

View file

@ -120,6 +120,15 @@ enum class Command(
isDevCommand = true,
isSupported = false,
),
CHANGE_AVATAR(
command = "/myavatar",
parameters = "<mxc_url>",
description = R.string.slash_command_description_avatar,
isAllowedInThread = false,
// Dev command since user has to know the mxc url
isDevCommand = true,
isSupported = false,
),
CHANGE_AVATAR_FOR_ROOM(
command = "/myroomavatar",
parameters = "<mxc_url>",

View file

@ -44,6 +44,7 @@ class CommandExecutor(
): Result<Unit> {
return when (slashCommand) {
is SlashCommand.BanUser -> banUser(slashCommand)
is SlashCommand.ChangeAvatar -> changeAvatar()
is SlashCommand.ChangeAvatarForRoom -> changeAvatarForRoom()
is SlashCommand.ChangeDisplayName -> changeDisplayName(slashCommand)
is SlashCommand.ChangeDisplayNameForRoom -> changeDisplayNameForRoom()
@ -178,6 +179,10 @@ class CommandExecutor(
return matrixClient.setDisplayName(slashCommand.displayName)
}
private fun changeAvatar(): Result<Unit> {
return Result.failure(Exception("Not yet implemented"))
}
private fun changeAvatarForRoom(): Result<Unit> {
return Result.failure(Exception("Not yet implemented"))
}

View file

@ -107,6 +107,18 @@ class CommandParser(
syntaxError(Command.ROOM_AVATAR)
}
}
Command.CHANGE_AVATAR.matches(slashCommand) -> {
if (messageParts.size == 2) {
val url = messageParts[1]
if (url.isMxcUrl()) {
SlashCommand.ChangeAvatar(url)
} else {
syntaxError(Command.CHANGE_AVATAR)
}
} else {
syntaxError(Command.CHANGE_AVATAR)
}
}
Command.CHANGE_AVATAR_FOR_ROOM.matches(slashCommand) -> {
if (messageParts.size == 2) {
val url = messageParts[1]

View file

@ -11,6 +11,7 @@ import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.slashcommands.api.SlashCommand
@ -18,6 +19,8 @@ import io.element.android.libraries.slashcommands.api.SlashCommandService
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds
@ContributesBinding(RoomScope::class)
class DefaultSlashCommandService(
@ -26,6 +29,7 @@ class DefaultSlashCommandService(
private val stringProvider: StringProvider,
private val appPreferencesStore: AppPreferencesStore,
private val featureFlagService: FeatureFlagService,
private val capabilitiesProvider: HomeserverCapabilitiesProvider,
) : SlashCommandService {
override suspend fun getSuggestions(
text: String,
@ -33,19 +37,41 @@ class DefaultSlashCommandService(
): List<SlashCommandSuggestion> {
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SlashCommand)) return emptyList()
val isDeveloperModeEnabled = appPreferencesStore.isDeveloperModeEnabledFlow().first()
return Command.entries.filter {
it.startsWith(text)
}.filter {
!isInThread || it.isAllowedInThread
}.filter {
!it.isDevCommand || isDeveloperModeEnabled
}.map {
SlashCommandSuggestion(
command = it.command,
parameters = it.parameters,
description = stringProvider.getString(it.description),
)
}
return Command.entries
.asSequence()
.filter { it.startsWith(text) }
.filter { !isInThread || it.isAllowedInThread }
.filter { !it.isDevCommand || isDeveloperModeEnabled }
// Don't include the change display name commands if the user can't change their display name
.run {
val canUserChangeDisplayName = withTimeoutOrNull(5.seconds) {
capabilitiesProvider.canChangeDisplayName().getOrNull()
} ?: false
if (!canUserChangeDisplayName) {
filterNot { it == Command.CHANGE_DISPLAY_NAME || it == Command.CHANGE_DISPLAY_NAME_FOR_ROOM }
} else {
this
}
}
// Don't include the change avatar commands if the user can't change their avatar url
.run {
val canUserChangeAvatar = withTimeoutOrNull(5.seconds) {
capabilitiesProvider.canChangeAvatarUrl().getOrNull()
} ?: false
if (!canUserChangeAvatar) {
filterNot { it == Command.CHANGE_AVATAR || it == Command.CHANGE_AVATAR_FOR_ROOM }
} else {
this
}
}
.map {
SlashCommandSuggestion(
command = it.command,
parameters = it.parameters,
description = stringProvider.getString(it.description),
)
}
.toList()
}
override suspend fun parse(

View file

@ -26,6 +26,7 @@
<string name="slash_command_description_topic">Set the room topic</string>
<string name="slash_command_description_remove_user">Removes user with given id from this room</string>
<string name="slash_command_description_nick">Changes your display nickname</string>
<string name="slash_command_description_avatar">Changes your profile picture in all rooms</string>
<string name="slash_command_confetti">Sends the given message with confetti</string>
<string name="slash_command_snow">Sends the given message with snowfall</string>
<string name="slash_command_description_plain">Sends a message as plain text, without interpreting it as markdown</string>