Supporting command in threads

This commit is contained in:
ariskotsomitopoulos 2021-11-16 14:59:30 +02:00
parent 8c539426e6
commit 4160688f83
12 changed files with 225 additions and 124 deletions

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.relation
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
@ -136,6 +137,8 @@ interface RelationService {
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
*/
fun replyInThread(rootThreadEventId: String,
replyInThreadText: CharSequence,
autoMarkdown: Boolean = false): Cancelable?
replyInThreadText: CharSequence,
msgType: String = MessageType.MSGTYPE_TEXT,
autoMarkdown: Boolean = false,
formattedText: String? = null): Cancelable?
}

View File

@ -159,12 +159,15 @@ internal class DefaultRelationService @AssistedInject constructor(
}
}
override fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, autoMarkdown: Boolean): Cancelable {
override fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, msgType: String, autoMarkdown: Boolean, formattedText: String?): Cancelable {
val event = eventFactory.createThreadTextEvent(
rootThreadEventId = rootThreadEventId,
roomId = roomId,
text = replyInThreadText.toString(),
autoMarkdown = autoMarkdown)
msgType = msgType,
autoMarkdown = autoMarkdown,
formattedText = formattedText
)
// .also {
// saveLocalEcho(it)
// }

View File

@ -343,13 +343,21 @@ internal class LocalEchoEventFactory @Inject constructor(
/**
* Creates a thread event related to the already existing event
*/
fun createThreadTextEvent(rootThreadEventId: String, roomId:String, text: String, autoMarkdown: Boolean): Event =
createEvent(
roomId,
EventType.MESSAGE,
createTextContent(text, autoMarkdown)
.toThreadTextContent(rootThreadEventId)
.toContent())
fun createThreadTextEvent(
rootThreadEventId: String,
roomId: String,
text: String,
msgType: String,
autoMarkdown: Boolean,
formattedText: String?): Event {
val content = formattedText?.let { TextContent(text, it) } ?: createTextContent(text, autoMarkdown)
return createEvent(
roomId,
EventType.MESSAGE,
content.toThreadTextContent(rootThreadEventId, msgType)
.toContent())
}
private fun dummyOriginServerTs(): Long {
return System.currentTimeMillis()

View File

@ -18,17 +18,30 @@ package im.vector.app.features.autocomplete.command
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.BuildConfig
import im.vector.app.features.autocomplete.AutocompleteClickListener
import im.vector.app.features.autocomplete.RecyclerViewPresenter
import im.vector.app.features.command.Command
import im.vector.app.features.home.room.detail.AutoCompleter
import im.vector.app.features.settings.VectorPreferences
import timber.log.Timber
import javax.inject.Inject
class AutocompleteCommandPresenter @Inject constructor(context: Context,
private val controller: AutocompleteCommandController,
private val vectorPreferences: VectorPreferences) :
class AutocompleteCommandPresenter @AssistedInject constructor(
@Assisted val isInThreadTimeline: Boolean,
context: Context,
private val controller: AutocompleteCommandController,
private val vectorPreferences: VectorPreferences) :
RecyclerViewPresenter<Command>(context), AutocompleteClickListener<Command> {
@AssistedFactory
interface Factory {
fun create(isFromThreadTimeline: Boolean): AutocompleteCommandPresenter
}
init {
controller.listener = this
}
@ -46,6 +59,12 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context,
.filter {
!it.isDevCommand || vectorPreferences.developerMode()
}
.filter {
if (BuildConfig.THREADING_ENABLED && isInThreadTimeline) {
it.isThreadCommand
} else
true
}
.filter {
if (query.isNullOrEmpty()) {
true

View File

@ -24,42 +24,42 @@ import im.vector.app.R
* the user can write theses messages to perform some actions
* the list will be displayed in this order
*/
enum class Command(val command: String, val parameters: String, @StringRes val description: Int, val isDevCommand: Boolean) {
EMOTE("/me", "<message>", R.string.command_description_emote, false),
BAN_USER("/ban", "<user-id> [reason]", R.string.command_description_ban_user, false),
UNBAN_USER("/unban", "<user-id> [reason]", R.string.command_description_unban_user, false),
IGNORE_USER("/ignore", "<user-id> [reason]", R.string.command_description_ignore_user, false),
UNIGNORE_USER("/unignore", "<user-id>", R.string.command_description_unignore_user, false),
SET_USER_POWER_LEVEL("/op", "<user-id> [<power-level>]", R.string.command_description_op_user, false),
RESET_USER_POWER_LEVEL("/deop", "<user-id>", R.string.command_description_deop_user, false),
ROOM_NAME("/roomname", "<name>", R.string.command_description_room_name, false),
INVITE("/invite", "<user-id> [reason]", R.string.command_description_invite_user, false),
JOIN_ROOM("/join", "<room-alias> [reason]", R.string.command_description_join_room, false),
PART("/part", "<room-alias> [reason]", R.string.command_description_part_room, false),
TOPIC("/topic", "<topic>", R.string.command_description_topic, false),
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user, false),
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick, false),
CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "<display-name>", R.string.command_description_nick_for_room, false),
ROOM_AVATAR("/roomavatar", "<mxc_url>", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */),
CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "<mxc_url>", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */),
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown, false),
RAINBOW("/rainbow", "<message>", R.string.command_description_rainbow, false),
RAINBOW_EMOTE("/rainbowme", "<message>", R.string.command_description_rainbow_emote, false),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token, false),
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler, false),
POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll, false),
SHRUG("/shrug", "<message>", R.string.command_description_shrug, false),
LENNY("/lenny", "<message>", R.string.command_description_lenny, false),
PLAIN("/plain", "<message>", R.string.command_description_plain, false),
WHOIS("/whois", "<user-id>", R.string.command_description_whois, false),
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false),
CONFETTI("/confetti", "<message>", R.string.command_confetti, false),
SNOWFALL("/snowfall", "<message>", R.string.command_snow, false),
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space, true),
ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true),
JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true),
LEAVE_ROOM("/leave", "<roomId?>", R.string.command_description_leave_room, true),
UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true);
enum class Command(val command: String, val parameters: String, @StringRes val description: Int, val isDevCommand: Boolean, val isThreadCommand: Boolean) {
EMOTE("/me", "<message>", R.string.command_description_emote, false, true),
BAN_USER("/ban", "<user-id> [reason]", R.string.command_description_ban_user, false, false),
UNBAN_USER("/unban", "<user-id> [reason]", R.string.command_description_unban_user, false, false),
IGNORE_USER("/ignore", "<user-id> [reason]", R.string.command_description_ignore_user, false, true),
UNIGNORE_USER("/unignore", "<user-id>", R.string.command_description_unignore_user, false, true),
SET_USER_POWER_LEVEL("/op", "<user-id> [<power-level>]", R.string.command_description_op_user, false, false),
RESET_USER_POWER_LEVEL("/deop", "<user-id>", R.string.command_description_deop_user, false, false),
ROOM_NAME("/roomname", "<name>", R.string.command_description_room_name, false, false),
INVITE("/invite", "<user-id> [reason]", R.string.command_description_invite_user, false, false),
JOIN_ROOM("/join", "<room-alias> [reason]", R.string.command_description_join_room, false, false),
PART("/part", "<room-alias> [reason]", R.string.command_description_part_room, false, false),
TOPIC("/topic", "<topic>", R.string.command_description_topic, false, false),
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user, false, false),
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick, false, false),
CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "<display-name>", R.string.command_description_nick_for_room, false, false),
ROOM_AVATAR("/roomavatar", "<mxc_url>", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */, false),
CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "<mxc_url>", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */, false),
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown, false, false),
RAINBOW("/rainbow", "<message>", R.string.command_description_rainbow, false, true),
RAINBOW_EMOTE("/rainbowme", "<message>", R.string.command_description_rainbow_emote, false, true),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token, false, false),
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler, false, true),
POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll, false, false),
SHRUG("/shrug", "<message>", R.string.command_description_shrug, false, true),
LENNY("/lenny", "<message>", R.string.command_description_lenny, false, true),
PLAIN("/plain", "<message>", R.string.command_description_plain, false, true),
WHOIS("/whois", "<user-id>", R.string.command_description_whois, false, true),
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false, false),
CONFETTI("/confetti", "<message>", R.string.command_confetti, false, false),
SNOWFALL("/snowfall", "<message>", R.string.command_snow, false, false),
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space, true, false),
ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true, false),
JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true, false),
LEAVE_ROOM("/leave", "<roomId?>", R.string.command_description_leave_room, true, false),
UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true, false);
val length
get() = command.length + 1

View File

@ -16,6 +16,7 @@
package im.vector.app.features.command
import im.vector.app.BuildConfig
import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.isMsisdn
import im.vector.app.features.home.room.detail.ChatEffect
@ -32,7 +33,7 @@ object CommandParser {
* @param textMessage the text message
* @return a parsed slash command (ok or error)
*/
fun parseSplashCommand(textMessage: CharSequence): ParsedCommand {
fun parseSplashCommand(textMessage: CharSequence, isInThreadTimeline: Boolean): ParsedCommand {
// check if it has the Slash marker
if (!textMessage.startsWith("/")) {
return ParsedCommand.ErrorNotACommand
@ -61,6 +62,20 @@ object CommandParser {
return ParsedCommand.ErrorEmptySlashCommand
}
// If the command is not supported by threads return error
if(BuildConfig.THREADING_ENABLED && isInThreadTimeline){
val slashCommand = messageParts.first()
val notSupportedCommandsInThreads = Command.values().filter {
!it.isThreadCommand
}.map {
it.command
}
if(notSupportedCommandsInThreads.contains(slashCommand)){
return ParsedCommand.ErrorCommandNotSupportedInThreads(slashCommand)
}
}
return when (val slashCommand = messageParts.first()) {
Command.PLAIN.command -> {
val text = textMessage.substring(Command.PLAIN.command.length).trim()

View File

@ -28,6 +28,8 @@ sealed class ParsedCommand {
object ErrorEmptySlashCommand : ParsedCommand()
class ErrorCommandNotSupportedInThreads(val slashCommand: String) : ParsedCommand()
// Unknown/Unsupported slash command
class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand()

View File

@ -49,9 +49,10 @@ import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
class AutoCompleter @AssistedInject constructor(
@Assisted val roomId: String,
@Assisted val isInThreadTimeline: Boolean,
private val avatarRenderer: AvatarRenderer,
private val commandAutocompletePolicy: CommandAutocompletePolicy,
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
AutocompleteCommandPresenterFactory: AutocompleteCommandPresenter.Factory,
private val autocompleteMemberPresenterFactory: AutocompleteMemberPresenter.Factory,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
@ -62,7 +63,11 @@ class AutoCompleter @AssistedInject constructor(
@AssistedFactory
interface Factory {
fun create(roomId: String): AutoCompleter
fun create(roomId: String, isInThreadTimeline: Boolean): AutoCompleter
}
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by lazy {
AutocompleteCommandPresenterFactory.create(isInThreadTimeline)
}
private var editText: EditText? = null

View File

@ -291,8 +291,9 @@ class RoomDetailFragment @Inject constructor(
}
private val autoCompleter: AutoCompleter by lazy {
autoCompleterFactory.create(roomDetailArgs.roomId)
autoCompleterFactory.create(roomDetailArgs.roomId, isThreadTimeLine())
}
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
private val debouncer = Debouncer(createUIHandler())
@ -396,10 +397,10 @@ class RoomDetailFragment @Inject constructor(
return@onEach
}
when (mode) {
is SendMode.REGULAR -> renderRegularMode(mode.text)
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
is SendMode.REGULAR -> renderRegularMode(mode.text)
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
}
}
@ -1466,24 +1467,27 @@ class RoomDetailFragment @Inject constructor(
private fun renderSendMessageResult(sendMessageResult: TextComposerViewEvents.SendMessageResult) {
when (sendMessageResult) {
is TextComposerViewEvents.SlashCommandHandled -> {
is TextComposerViewEvents.SlashCommandHandled -> {
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
}
is TextComposerViewEvents.SlashCommandError -> {
is TextComposerViewEvents.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
}
is TextComposerViewEvents.SlashCommandUnknown -> {
is TextComposerViewEvents.SlashCommandUnknown -> {
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
}
is TextComposerViewEvents.SlashCommandResultOk -> {
is TextComposerViewEvents.SlashCommandResultOk -> {
views.composerLayout.setTextIfDifferent("")
}
is TextComposerViewEvents.SlashCommandResultError -> {
is TextComposerViewEvents.SlashCommandResultError -> {
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
}
is TextComposerViewEvents.SlashCommandNotImplemented -> {
is TextComposerViewEvents.SlashCommandNotImplemented -> {
displayCommandError(getString(R.string.not_implemented))
}
is TextComposerViewEvents.SlashCommandNotSupportedInThreads -> {
displayCommandError(getString(R.string.command_not_supported_in_threads, sendMessageResult.command))
}
} // .exhaustive
lockSendButton = false
@ -2217,6 +2221,6 @@ class RoomDetailFragment @Inject constructor(
}
}
fun isThreadTimeLine(): Boolean = roomDetailArgs.roomThreadDetailArgs != null
private fun isThreadTimeLine(): Boolean = roomDetailArgs.roomThreadDetailArgs != null
fun getRootThreadEventId(): String? = roomDetailArgs.roomThreadDetailArgs?.eventId
}

View File

@ -32,6 +32,7 @@ sealed class TextComposerViewEvents : VectorViewEvents {
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
class SlashCommandError(val command: Command) : SendMessageResult()
class SlashCommandUnknown(val command: String) : SendMessageResult()
class SlashCommandNotSupportedInThreads(val command: String) : SendMessageResult()
data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult()
object SlashCommandResultOk : SendMessageResult()
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()

View File

@ -152,160 +152,198 @@ class TextComposerViewModel @AssistedInject constructor(
private fun handleSendMessage(action: TextComposerAction.SendMessage) {
withState { state ->
when (state.sendMode) {
is SendMode.REGULAR -> {
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) {
is ParsedCommand.ErrorNotACommand -> {
is SendMode.REGULAR -> {
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text, state.isInThreadTimeline())) {
is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room
if (state.rootThreadEventId != null)
room.replyInThread(state.rootThreadEventId, action.text.toString(), action.autoMarkdown)
room.replyInThread(
rootThreadEventId = state.rootThreadEventId,
replyInThreadText = action.text.toString(),
autoMarkdown = action.autoMarkdown)
else
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_viewEvents.post(TextComposerViewEvents.MessageSent)
popDraft()
}
is ParsedCommand.ErrorSyntax -> {
is ParsedCommand.ErrorSyntax -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandError(slashCommandResult.command))
}
is ParsedCommand.ErrorEmptySlashCommand -> {
is ParsedCommand.ErrorEmptySlashCommand -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown("/"))
}
is ParsedCommand.ErrorUnknownSlashCommand -> {
is ParsedCommand.ErrorUnknownSlashCommand -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
}
is ParsedCommand.SendPlainText -> {
is ParsedCommand.ErrorCommandNotSupportedInThreads -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandNotSupportedInThreads(slashCommandResult.slashCommand))
}
is ParsedCommand.SendPlainText -> {
// Send the text message to the room, without markdown
room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
if (state.rootThreadEventId != null)
room.replyInThread(
rootThreadEventId = state.rootThreadEventId,
replyInThreadText = action.text.toString(),
autoMarkdown = false)
else
room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
_viewEvents.post(TextComposerViewEvents.MessageSent)
popDraft()
}
is ParsedCommand.ChangeRoomName -> {
is ParsedCommand.ChangeRoomName -> {
handleChangeRoomNameSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.Invite -> {
is ParsedCommand.Invite -> {
handleInviteSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.Invite3Pid -> {
is ParsedCommand.Invite3Pid -> {
handleInvite3pidSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.SetUserPowerLevel -> {
is ParsedCommand.SetUserPowerLevel -> {
handleSetUserPowerLevel(slashCommandResult)
popDraft()
}
is ParsedCommand.ClearScalarToken -> {
is ParsedCommand.ClearScalarToken -> {
// TODO
_viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.SetMarkdown -> {
is ParsedCommand.SetMarkdown -> {
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled(
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
popDraft()
}
is ParsedCommand.BanUser -> {
is ParsedCommand.BanUser -> {
handleBanSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.UnbanUser -> {
is ParsedCommand.UnbanUser -> {
handleUnbanSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.IgnoreUser -> {
is ParsedCommand.IgnoreUser -> {
handleIgnoreSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.UnignoreUser -> {
is ParsedCommand.UnignoreUser -> {
handleUnignoreSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.KickUser -> {
is ParsedCommand.KickUser -> {
handleKickSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.JoinRoom -> {
is ParsedCommand.JoinRoom -> {
handleJoinToAnotherRoomSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.PartRoom -> {
is ParsedCommand.PartRoom -> {
// TODO
_viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown)
is ParsedCommand.SendEmote -> {
state.rootThreadEventId?.let {
room.replyInThread(
rootThreadEventId = state.rootThreadEventId,
replyInThreadText = slashCommandResult.message,
msgType = MessageType.MSGTYPE_EMOTE,
autoMarkdown = action.autoMarkdown)
} ?: room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown)
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendRainbow -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
}
is ParsedCommand.SendRainbow -> {
val message = slashCommandResult.message.toString()
state.rootThreadEventId?.let {
room.replyInThread(
rootThreadEventId = state.rootThreadEventId,
replyInThreadText = slashCommandResult.message,
formattedText = rainbowGenerator.generate(message))
} ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message))
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendRainbowEmote -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
}
is ParsedCommand.SendRainbowEmote -> {
val message = slashCommandResult.message.toString()
state.rootThreadEventId?.let {
room.replyInThread(
rootThreadEventId = state.rootThreadEventId,
replyInThreadText = slashCommandResult.message,
msgType = MessageType.MSGTYPE_EMOTE,
formattedText = rainbowGenerator.generate(message))
} ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message),MessageType.MSGTYPE_EMOTE)
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendSpoiler -> {
room.sendFormattedTextMessage(
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
"<span data-mx-spoiler>${slashCommandResult.message}</span>"
is ParsedCommand.SendSpoiler -> {
val text = "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})"
val formattedText = "<span data-mx-spoiler>${slashCommandResult.message}</span>"
state.rootThreadEventId?.let {
room.replyInThread(
rootThreadEventId = state.rootThreadEventId,
replyInThreadText = text,
formattedText = formattedText)
} ?: room.sendFormattedTextMessage(
text,
formattedText
)
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendShrug -> {
sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message)
is ParsedCommand.SendShrug -> {
sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message, state.rootThreadEventId)
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendLenny -> {
sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message)
is ParsedCommand.SendLenny -> {
sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message, state.rootThreadEventId)
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendChatEffect -> {
is ParsedCommand.SendChatEffect -> {
sendChatEffect(slashCommandResult)
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendPoll -> {
is ParsedCommand.SendPoll -> {
room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") })
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.ChangeTopic -> {
is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.ChangeDisplayName -> {
is ParsedCommand.ChangeDisplayName -> {
handleChangeDisplayNameSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.ChangeDisplayNameForRoom -> {
is ParsedCommand.ChangeDisplayNameForRoom -> {
handleChangeDisplayNameForRoomSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.ChangeRoomAvatar -> {
is ParsedCommand.ChangeRoomAvatar -> {
handleChangeRoomAvatarSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.ChangeAvatarForRoom -> {
is ParsedCommand.ChangeAvatarForRoom -> {
handleChangeAvatarForRoomSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.ShowUser -> {
is ParsedCommand.ShowUser -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
handleWhoisSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.DiscardSession -> {
is ParsedCommand.DiscardSession -> {
if (room.isEncrypted()) {
session.cryptoService().discardOutboundSession(room.roomId)
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
@ -318,7 +356,7 @@ class TextComposerViewModel @AssistedInject constructor(
)
}
}
is ParsedCommand.CreateSpace -> {
is ParsedCommand.CreateSpace -> {
viewModelScope.launch(Dispatchers.IO) {
try {
val params = CreateSpaceParams().apply {
@ -340,7 +378,7 @@ class TextComposerViewModel @AssistedInject constructor(
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.AddToSpace -> {
is ParsedCommand.AddToSpace -> {
viewModelScope.launch(Dispatchers.IO) {
try {
session.spaceService().getSpace(slashCommandResult.spaceId)
@ -357,7 +395,7 @@ class TextComposerViewModel @AssistedInject constructor(
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.JoinSpace -> {
is ParsedCommand.JoinSpace -> {
viewModelScope.launch(Dispatchers.IO) {
try {
session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias)
@ -368,7 +406,7 @@ class TextComposerViewModel @AssistedInject constructor(
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.LeaveRoom -> {
is ParsedCommand.LeaveRoom -> {
viewModelScope.launch(Dispatchers.IO) {
try {
session.getRoom(slashCommandResult.roomId)?.leave(null)
@ -379,7 +417,7 @@ class TextComposerViewModel @AssistedInject constructor(
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.UpgradeRoom -> {
is ParsedCommand.UpgradeRoom -> {
_viewEvents.post(
TextComposerViewEvents.ShowRoomUpgradeDialog(
slashCommandResult.newVersion,
@ -391,7 +429,7 @@ class TextComposerViewModel @AssistedInject constructor(
}
}.exhaustive
}
is SendMode.EDIT -> {
is SendMode.EDIT -> {
// is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
if (inReplyTo != null) {
@ -414,7 +452,7 @@ class TextComposerViewModel @AssistedInject constructor(
_viewEvents.post(TextComposerViewEvents.MessageSent)
popDraft()
}
is SendMode.QUOTE -> {
is SendMode.QUOTE -> {
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
val textMsg = messageContent?.body
@ -435,7 +473,7 @@ class TextComposerViewModel @AssistedInject constructor(
_viewEvents.post(TextComposerViewEvents.MessageSent)
popDraft()
}
is SendMode.REPLY -> {
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_viewEvents.post(TextComposerViewEvents.MessageSent)
@ -657,7 +695,7 @@ class TextComposerViewModel @AssistedInject constructor(
_viewEvents.post(TextComposerViewEvents.OpenRoomMemberProfile(whois.userId))
}
private fun sendPrefixedMessage(prefix: String, message: CharSequence) {
private fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) {
val sequence = buildString {
append(prefix)
if (message.isNotEmpty()) {
@ -665,7 +703,9 @@ class TextComposerViewModel @AssistedInject constructor(
append(message)
}
}
room.sendTextMessage(sequence)
rootThreadEventId?.let {
room.replyInThread(it, sequence)
}?: room.sendTextMessage(sequence)
}
/**

View File

@ -1831,6 +1831,7 @@
<string name="command_error">Command error</string>
<string name="unrecognized_command">Unrecognized command: %s</string>
<string name="command_problem_with_parameters">The command \"%s\" needs more parameters, or some parameters are incorrect.</string>
<string name="command_not_supported_in_threads">The command \"%s\" is recognized but not supported in threads.</string>
<string name="command_description_emote">Displays action</string>
<string name="command_description_ban_user">Bans user with given id</string>
<string name="command_description_unban_user">Unbans user with given id</string>