diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index a722729629..d3c70b01c0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -119,6 +119,8 @@ import im.vector.app.core.utils.startInstallFromSourceIntent import im.vector.app.core.utils.toast import im.vector.app.databinding.DialogReportContentBinding import im.vector.app.databinding.FragmentTimelineBinding +import im.vector.app.features.MainActivity +import im.vector.app.features.MainActivityArgs import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.MobileScreen @@ -136,6 +138,7 @@ import im.vector.app.features.call.conference.ConferenceEventObserver import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.Command +import im.vector.app.features.command.ParsedCommand import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer @@ -437,6 +440,7 @@ class TimelineFragment @Inject constructor( messageComposerViewModel.observeViewEvents { when (it) { is MessageComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) + is MessageComposerViewEvents.SlashCommandConfirmationRequest -> handleSlashCommandConfirmationRequest(it) is MessageComposerViewEvents.SendMessageResult -> renderSendMessageResult(it) is MessageComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message) is MessageComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) @@ -495,6 +499,25 @@ class TimelineFragment @Inject constructor( } } + private fun handleSlashCommandConfirmationRequest(action: MessageComposerViewEvents.SlashCommandConfirmationRequest) { + when (action.parsedCommand) { + is ParsedCommand.UnignoreUser -> promptUnignoreUser(action.parsedCommand) + else -> TODO("Add case for ${action.parsedCommand.javaClass.simpleName}") + } + lockSendButton = false + } + + private fun promptUnignoreUser(command: ParsedCommand.UnignoreUser) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.room_participants_action_unignore_title) + .setMessage(getString(R.string.settings_unignore_user, command.userId)) + .setPositiveButton(R.string.unignore) { _, _ -> + messageComposerViewModel.handle(MessageComposerAction.SlashCommandConfirmed(command)) + } + .setNegativeButton(R.string.action_cancel, null) + .show() + } + private fun renderVoiceMessageMode(content: String) { ContentAttachmentData.fromJsonString(content)?.let { audioAttachmentData -> views.voiceMessageRecorderView.isVisible = true @@ -1679,9 +1702,7 @@ class TimelineFragment @Inject constructor( displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } is MessageComposerViewEvents.SlashCommandResultOk -> { - dismissLoadingDialog() - views.composerLayout.setTextIfDifferent("") - sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } + handleSlashCommandResultOk(sendMessageResult.parsedCommand) } is MessageComposerViewEvents.SlashCommandResultError -> { dismissLoadingDialog() @@ -1698,6 +1719,21 @@ class TimelineFragment @Inject constructor( lockSendButton = false } + private fun handleSlashCommandResultOk(parsedCommand: ParsedCommand) { + dismissLoadingDialog() + views.composerLayout.setTextIfDifferent("") + when (parsedCommand) { + is ParsedCommand.SetMarkdown -> { + showSnackWithMessage(getString(if (parsedCommand.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) + } + is ParsedCommand.UnignoreUser -> { + // A user has been un-ignored, perform a initial sync + MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true)) + } + else -> Unit + } + } + private fun displayCommandError(message: String) { MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.command_error) @@ -2411,23 +2447,23 @@ class TimelineFragment @Inject constructor( } private fun displayThreadsBetaOptInDialog() { - activity?.let { - MaterialAlertDialogBuilder(it) - .setTitle(R.string.threads_beta_enable_notice_title) - .setMessage(threadsManager.getBetaEnableThreadsMessage()) - .setCancelable(true) - .setNegativeButton(R.string.action_not_now) { _, _ -> } - .setPositiveButton(R.string.action_try_it_out) { _, _ -> - threadsManager.enableThreadsAndRestart(it) - } - .show() - ?.findViewById(android.R.id.message) - ?.apply { - linksClickable = true - movementMethod = LinkMovementMethod.getInstance() - } - } + activity?.let { + MaterialAlertDialogBuilder(it) + .setTitle(R.string.threads_beta_enable_notice_title) + .setMessage(threadsManager.getBetaEnableThreadsMessage()) + .setCancelable(true) + .setNegativeButton(R.string.action_not_now) { _, _ -> } + .setPositiveButton(R.string.action_try_it_out) { _, _ -> + threadsManager.enableThreadsAndRestart(it) + } + .show() + ?.findViewById(android.R.id.message) + ?.apply { + linksClickable = true + movementMethod = LinkMovementMethod.getInstance() + } } + } /** * Navigate to Threads list for the current room diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt index dca698ee52..0da324ffc2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.detail.composer import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.command.ParsedCommand import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent @@ -30,6 +31,7 @@ sealed class MessageComposerAction : VectorViewModelAction { data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction() data class OnTextChanged(val text: CharSequence) : MessageComposerAction() data class OnEntersBackground(val composerText: String) : MessageComposerAction() + data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction() // Voice Message data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt index 0b8ab1fe04..fd302005c6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.composer import androidx.annotation.StringRes import im.vector.app.core.platform.VectorViewEvents import im.vector.app.features.command.Command +import im.vector.app.features.command.ParsedCommand sealed class MessageComposerViewEvents : VectorViewEvents { @@ -35,9 +36,11 @@ sealed class MessageComposerViewEvents : VectorViewEvents { data class SlashCommandNotSupportedInThreads(val command: Command) : SendMessageResult() data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() object SlashCommandLoading : SendMessageResult() - data class SlashCommandResultOk(@StringRes val messageRes: Int? = null) : SendMessageResult() + data class SlashCommandResultOk(val parsedCommand: ParsedCommand) : SendMessageResult() data class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() + data class SlashCommandConfirmationRequest(val parsedCommand: ParsedCommand) : MessageComposerViewEvents() + data class OpenRoomMemberProfile(val userId: String) : MessageComposerViewEvents() // TODO Remove diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 9a82d26416..9c81a39941 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -110,6 +110,7 @@ class MessageComposerViewModel @AssistedInject constructor( is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action) is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action) + is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action) } } @@ -255,8 +256,7 @@ class MessageComposerViewModel @AssistedInject constructor( } is ParsedCommand.SetMarkdown -> { vectorPreferences.setMarkdownEnabled(parsedCommand.enable) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk( - if (parsedCommand.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } is ParsedCommand.BanUser -> { @@ -291,7 +291,7 @@ class MessageComposerViewModel @AssistedInject constructor( } else { room.sendTextMessage(parsedCommand.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) } - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } is ParsedCommand.SendRainbow -> { @@ -304,7 +304,7 @@ class MessageComposerViewModel @AssistedInject constructor( } else { room.sendFormattedTextMessage(message, rainbowGenerator.generate(message)) } - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } is ParsedCommand.SendRainbowEmote -> { @@ -319,7 +319,7 @@ class MessageComposerViewModel @AssistedInject constructor( room.sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE) } - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } is ParsedCommand.SendSpoiler -> { @@ -335,22 +335,22 @@ class MessageComposerViewModel @AssistedInject constructor( text, formattedText) } - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } is ParsedCommand.SendShrug -> { sendPrefixedMessage("¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } is ParsedCommand.SendLenny -> { sendPrefixedMessage("( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } is ParsedCommand.SendChatEffect -> { sendChatEffect(parsedCommand) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } is ParsedCommand.ChangeTopic -> { @@ -369,17 +369,17 @@ class MessageComposerViewModel @AssistedInject constructor( handleChangeAvatarForRoomSlashCommand(parsedCommand) } is ParsedCommand.ShowUser -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) handleWhoisSlashCommand(parsedCommand) popDraft() } is ParsedCommand.DiscardSession -> { if (room.isEncrypted()) { session.cryptoService().discardOutboundSession(room.roomId) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } else { - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post( MessageComposerViewEvents .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled)) @@ -403,7 +403,7 @@ class MessageComposerViewModel @AssistedInject constructor( true ) popDraft() - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) } @@ -422,7 +422,7 @@ class MessageComposerViewModel @AssistedInject constructor( false ) popDraft() - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) } @@ -435,7 +435,7 @@ class MessageComposerViewModel @AssistedInject constructor( try { session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias) popDraft() - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) } @@ -447,7 +447,7 @@ class MessageComposerViewModel @AssistedInject constructor( try { session.leaveRoom(parsedCommand.roomId) popDraft() - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) } @@ -461,7 +461,7 @@ class MessageComposerViewModel @AssistedInject constructor( room.roomSummary()?.isPublic ?: false ) ) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } } @@ -644,19 +644,19 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(changeTopic) { room.updateTopic(changeTopic.topic) } } private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(invite) { room.invite(invite.userId, invite.reason) } } private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(invite) { room.invite3pid(invite.threePid) } } @@ -669,19 +669,19 @@ class MessageComposerViewModel @AssistedInject constructor( ?.toContent() ?: return - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(setUserPowerLevel) { room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent) } } private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(changeDisplayName) { session.setDisplayName(session.myUserId, changeDisplayName.displayName) } } private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(command) { if (command.roomAlias == null) { // Leave the current room room @@ -697,25 +697,25 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(removeUser) { room.remove(removeUser.userId, removeUser.reason) } } private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(ban) { room.ban(ban.userId, ban.reason) } } private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(unban) { room.unban(unban.userId, unban.reason) } } private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(changeRoomName) { room.updateName(changeRoomName.name) } } @@ -727,7 +727,7 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(changeDisplayName) { getMyRoomMemberContent() ?.copy(displayName = changeDisplayName.displayName) ?.toContent() @@ -738,13 +738,13 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(changeAvatar) { room.sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent()) } } private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(changeAvatar) { getMyRoomMemberContent() ?.copy(avatarUrl = changeAvatar.url) ?.toContent() @@ -755,13 +755,24 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) { - launchSlashCommandFlowSuspendable { + launchSlashCommandFlowSuspendable(ignore) { session.ignoreUserIds(listOf(ignore.userId)) } } private fun handleUnignoreSlashCommand(unignore: ParsedCommand.UnignoreUser) { - launchSlashCommandFlowSuspendable { + _viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore)) + } + + private fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) { + when (action.parsedCommand) { + is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand) + else -> TODO("Not handled yet") + } + } + + private fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) { + launchSlashCommandFlowSuspendable(unignore) { session.unIgnoreUserIds(listOf(unignore.userId)) } } @@ -900,13 +911,13 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { + private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) { _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) viewModelScope.launch { val event = try { block() popDraft() - MessageComposerViewEvents.SlashCommandResultOk() + MessageComposerViewEvents.SlashCommandResultOk(parsedCommand) } catch (failure: Throwable) { MessageComposerViewEvents.SlashCommandResultError(failure) }