diff --git a/changelog.d/2909.feature b/changelog.d/2909.feature new file mode 100644 index 0000000000..4d72734192 --- /dev/null +++ b/changelog.d/2909.feature @@ -0,0 +1 @@ +Implement /part command, with or without parameter \ No newline at end of file diff --git a/changelog.d/4261.bugfix b/changelog.d/4261.bugfix new file mode 100644 index 0000000000..4283335131 --- /dev/null +++ b/changelog.d/4261.bugfix @@ -0,0 +1 @@ +Fix crash on slash commands Exceptions \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 33ccd08d22..ccabd25ff4 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -34,8 +34,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user, false), ROOM_NAME("/roomname", "", R.string.command_description_room_name, false), INVITE("/invite", " [reason]", R.string.command_description_invite_user, false), - JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room, false), - PART("/part", " [reason]", R.string.command_description_part_room, false), + JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room, false), + PART("/part", "[]", R.string.command_description_part_room, false), TOPIC("/topic", "", R.string.command_description_topic, false), KICK_USER("/kick", " [reason]", R.string.command_description_kick_user, false), CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick, false), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index e570033d35..3a6b005d6f 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -158,21 +158,10 @@ object CommandParser { } } Command.PART.command -> { - if (messageParts.size >= 2) { - val roomAlias = messageParts[1] - - if (roomAlias.isNotEmpty()) { - ParsedCommand.PartRoom( - roomAlias, - textMessage.substring(Command.PART.length + roomAlias.length) - .trim() - .takeIf { it.isNotBlank() } - ) - } else { - ParsedCommand.ErrorSyntax(Command.PART) - } - } else { - ParsedCommand.ErrorSyntax(Command.PART) + when (messageParts.size) { + 1 -> ParsedCommand.PartRoom(null) + 2 -> ParsedCommand.PartRoom(messageParts[1]) + else -> ParsedCommand.ErrorSyntax(Command.PART) } } Command.ROOM_NAME.command -> { diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index bafb9153e6..89aa8d9188 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -49,7 +49,7 @@ sealed class ParsedCommand { class Invite(val userId: String, val reason: String?) : ParsedCommand() class Invite3Pid(val threePid: ThreePid) : ParsedCommand() class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand() - class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand() + class PartRoom(val roomAlias: String?) : ParsedCommand() class ChangeTopic(val topic: String) : ParsedCommand() class KickUser(val userId: String, val reason: String?) : ParsedCommand() class ChangeDisplayName(val displayName: String) : ParsedCommand() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index e9948e6cf4..473a993395 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1451,8 +1451,8 @@ class RoomDetailFragment @Inject constructor( private fun renderSendMessageResult(sendMessageResult: TextComposerViewEvents.SendMessageResult) { when (sendMessageResult) { - is TextComposerViewEvents.SlashCommandHandled -> { - sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } + is TextComposerViewEvents.SlashCommandLoading -> { + showLoading(null) } is TextComposerViewEvents.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) @@ -1461,9 +1461,12 @@ class RoomDetailFragment @Inject constructor( displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } is TextComposerViewEvents.SlashCommandResultOk -> { + dismissLoadingDialog() views.composerLayout.setTextIfDifferent("") + sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } } is TextComposerViewEvents.SlashCommandResultError -> { + dismissLoadingDialog() displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable)) } is TextComposerViewEvents.SlashCommandNotImplemented -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt index 691ed4d93e..ff4a09ad71 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt @@ -32,8 +32,8 @@ sealed class TextComposerViewEvents : VectorViewEvents { data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() class SlashCommandError(val command: Command) : SendMessageResult() class SlashCommandUnknown(val command: String) : SendMessageResult() - data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() - object SlashCommandResultOk : SendMessageResult() + object SlashCommandLoading : SendMessageResult() + data class SlashCommandResultOk(@StringRes val messageRes: Int? = null) : SendMessageResult() class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() data class OpenRoomMemberProfile(val userId: String) : TextComposerViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index 742d2848a1..b635602189 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -176,19 +176,15 @@ class TextComposerViewModel @AssistedInject constructor( } is ParsedCommand.ChangeRoomName -> { handleChangeRoomNameSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.Invite3Pid -> { handleInvite3pidSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.SetUserPowerLevel -> { handleSetUserPowerLevel(slashCommandResult) - popDraft() } is ParsedCommand.ClearScalarToken -> { // TODO @@ -196,55 +192,49 @@ class TextComposerViewModel @AssistedInject constructor( } is ParsedCommand.SetMarkdown -> { vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled( + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk( if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) popDraft() } is ParsedCommand.BanUser -> { handleBanSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.UnbanUser -> { handleUnbanSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.IgnoreUser -> { handleIgnoreSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.UnignoreUser -> { handleUnignoreSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.KickUser -> { handleKickSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.JoinRoom -> { handleJoinToAnotherRoomSlashCommand(slashCommandResult) popDraft() } is ParsedCommand.PartRoom -> { - // TODO - _viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented) + handlePartSlashCommand(slashCommandResult) } is ParsedCommand.SendEmote -> { room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.SendRainbow -> { slashCommandResult.message.toString().let { room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) } - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.SendRainbowEmote -> { slashCommandResult.message.toString().let { room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) } - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.SendSpoiler -> { @@ -252,61 +242,56 @@ class TextComposerViewModel @AssistedInject constructor( "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", "${slashCommandResult.message}" ) - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.SendShrug -> { sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message) - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.SendLenny -> { sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message) - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.SendChatEffect -> { sendChatEffect(slashCommandResult) - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.SendPoll -> { room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") }) - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.ChangeDisplayName -> { handleChangeDisplayNameSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.ChangeDisplayNameForRoom -> { handleChangeDisplayNameForRoomSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.ChangeRoomAvatar -> { handleChangeRoomAvatarSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.ChangeAvatarForRoom -> { handleChangeAvatarForRoomSlashCommand(slashCommandResult) - popDraft() } is ParsedCommand.ShowUser -> { - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) handleWhoisSlashCommand(slashCommandResult) popDraft() } is ParsedCommand.DiscardSession -> { if (room.isEncrypted()) { session.cryptoService().discardOutboundSession(room.roomId) - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } else { - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post( TextComposerViewEvents .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled)) @@ -314,6 +299,7 @@ class TextComposerViewModel @AssistedInject constructor( } } is ParsedCommand.CreateSpace -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandLoading) viewModelScope.launch(Dispatchers.IO) { try { val params = CreateSpaceParams().apply { @@ -328,14 +314,16 @@ class TextComposerViewModel @AssistedInject constructor( null, true ) + popDraft() + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) } catch (failure: Throwable) { _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) } } - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) - popDraft() + Unit } is ParsedCommand.AddToSpace -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandLoading) viewModelScope.launch(Dispatchers.IO) { try { session.spaceService().getSpace(slashCommandResult.spaceId) @@ -345,34 +333,38 @@ class TextComposerViewModel @AssistedInject constructor( null, false ) + popDraft() + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) } catch (failure: Throwable) { _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) } } - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) - popDraft() + Unit } is ParsedCommand.JoinSpace -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandLoading) viewModelScope.launch(Dispatchers.IO) { try { session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias) + popDraft() + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) } catch (failure: Throwable) { _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) } } - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) - popDraft() + Unit } is ParsedCommand.LeaveRoom -> { viewModelScope.launch(Dispatchers.IO) { try { session.getRoom(slashCommandResult.roomId)?.leave(null) + popDraft() + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) } catch (failure: Throwable) { _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) } } - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) - popDraft() + Unit } is ParsedCommand.UpgradeRoom -> { _viewEvents.post( @@ -381,7 +373,7 @@ class TextComposerViewModel @AssistedInject constructor( room.roomSummary()?.isPublic ?: false ) ) - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) popDraft() } }.exhaustive @@ -578,6 +570,20 @@ class TextComposerViewModel @AssistedInject constructor( } } + private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) { + launchSlashCommandFlowSuspendable { + if (command.roomAlias == null) { + // Leave the current room + room + } else { + session.getRoomSummary(roomIdOrAlias = command.roomAlias) + ?.roomId + ?.let { session.getRoom(it) } + } + ?.leave(reason = null) + } + } + private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) { launchSlashCommandFlowSuspendable { room.kick(kick.userId, kick.reason) @@ -690,12 +696,13 @@ class TextComposerViewModel @AssistedInject constructor( } private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { - _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post(TextComposerViewEvents.SlashCommandLoading) viewModelScope.launch { val event = try { block() - TextComposerViewEvents.SlashCommandResultOk - } catch (failure: Exception) { + popDraft() + TextComposerViewEvents.SlashCommandResultOk() + } catch (failure: Throwable) { TextComposerViewEvents.SlashCommandResultError(failure) } _viewEvents.post(event) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 7fa4918266..274753ee3f 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1837,7 +1837,7 @@ Deops user with given id Sets the room name Invites user with given id to current room - Joins room with given alias + Joins room with given address Leave room Set the room topic Kicks user with given id