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:
parent
15419bb675
commit
b66df37f3e
26 changed files with 363 additions and 14 deletions
|
|
@ -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>",
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -78,12 +78,19 @@ class CommandParserTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun parseSlashCommandPlainAndNick() = runTest {
|
||||
fun parseSlashCommandPlain() = runTest {
|
||||
test("/plain hello", SlashCommand.SendPlainText("hello"))
|
||||
test("/plain", SlashCommand.ErrorSyntax("A string/plain, /plain <message>"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseSlashCommandNickAndMyAvatar() = runTest {
|
||||
test("/nick John", SlashCommand.ChangeDisplayName("John"))
|
||||
test("/nick", SlashCommand.ErrorSyntax("A string/nick, /nick <display-name>"))
|
||||
|
||||
test("/myavatar mxc://matrix.org/abc", SlashCommand.ChangeAvatar("mxc://matrix.org/abc"))
|
||||
test("/myavatar http://notmxc", SlashCommand.ErrorSyntax("A string/myavatar, /myavatar <mxc_url>"))
|
||||
test("/myavatar", SlashCommand.ErrorSyntax("A string/myavatar, /myavatar <mxc_url>"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||
import io.element.android.libraries.matrix.api.timeline.MsgType
|
||||
import io.element.android.libraries.matrix.test.FakeHomeserverCapabilitiesProvider
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
|
|
@ -116,6 +117,26 @@ class DefaultSlashCommandServiceTest {
|
|||
sendMessage.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canChangeDisplayName is respected in suggestions`() = runTest {
|
||||
var result = false
|
||||
val capabilitiesProvider = FakeHomeserverCapabilitiesProvider(
|
||||
canChangeDisplayName = { Result.success(result) },
|
||||
)
|
||||
val sut = createDefaultSlashCommandService(capabilitiesProvider = capabilitiesProvider)
|
||||
|
||||
// Initially, with a disabled capability, the change display name command should not be in the suggestions
|
||||
var changeNameCommand = sut.getSuggestions("", isInThread = false)
|
||||
.find { it.command == Command.CHANGE_DISPLAY_NAME.command }
|
||||
assertThat(changeNameCommand).isNull()
|
||||
|
||||
// When the capability is true, the command should be included in the suggestions
|
||||
result = true
|
||||
changeNameCommand = sut.getSuggestions("", isInThread = false)
|
||||
.find { it.command == Command.CHANGE_DISPLAY_NAME.command }
|
||||
assertThat(changeNameCommand).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `proceedAdmin delegates to commandExecutor`() = runTest {
|
||||
val leaveRoomLambda = lambdaRecorder<Result<Unit>> {
|
||||
|
|
@ -155,11 +176,13 @@ class DefaultSlashCommandServiceTest {
|
|||
commandExecutor: CommandExecutor = createCommandExecutor(
|
||||
stringProvider = stringProvider,
|
||||
),
|
||||
capabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(),
|
||||
) = DefaultSlashCommandService(
|
||||
commandParser = commandParser,
|
||||
commandExecutor = commandExecutor,
|
||||
stringProvider = stringProvider,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
featureFlagService = featureFlagService,
|
||||
capabilitiesProvider = capabilitiesProvider,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue