Fix crash in message composer when room is missing (#7683)

This error was seen before but has been reintroduced during refactoring.
- see https://github.com/vector-im/element-android/pull/6978
This commit is contained in:
jonnyandrew 2022-12-02 08:41:33 +00:00 committed by GitHub
parent 4c58cc877f
commit 20b1eaba9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 127 additions and 114 deletions

2
changelog.d/7683.bugfix Normal file
View File

@ -0,0 +1,2 @@
Fix crash in message composer when room is missing

View File

@ -59,6 +59,7 @@ import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
@ -89,39 +90,44 @@ class MessageComposerViewModel @AssistedInject constructor(
private val voiceBroadcastHelper: VoiceBroadcastHelper, private val voiceBroadcastHelper: VoiceBroadcastHelper,
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) { ) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)
// Keep it out of state to avoid invalidate being called // Keep it out of state to avoid invalidate being called
private var currentComposerText: CharSequence = "" private var currentComposerText: CharSequence = ""
init { init {
loadDraftIfAny() if (room != null) {
observePowerLevelAndEncryption() loadDraftIfAny(room)
observeVoiceBroadcast() observePowerLevelAndEncryption(room)
observeVoiceBroadcast(room)
subscribeToStateInternal() subscribeToStateInternal()
} else {
onRoomError()
}
} }
override fun handle(action: MessageComposerAction) { override fun handle(action: MessageComposerAction) {
val room = this.room ?: return
when (action) { when (action) {
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action) is MessageComposerAction.EnterEditMode -> handleEnterEditMode(room, action)
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(room, action)
is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action) is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action) is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(room, action)
is MessageComposerAction.SendMessage -> handleSendMessage(action) is MessageComposerAction.SendMessage -> handleSendMessage(room, action)
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action) is MessageComposerAction.UserIsTyping -> handleUserIsTyping(room, action)
is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action) is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action)
is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action) is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage() is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage(room)
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled, action.rootThreadEventId) is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(room, action.isCancelled, action.rootThreadEventId)
is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action) is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage() MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback() MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData) is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(room, action.attachmentData)
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText) is MessageComposerAction.OnEntersBackground -> handleEntersBackground(room, action.composerText)
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action) is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action) is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action) is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(room, action)
is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action) is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action) is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action)
} }
@ -157,7 +163,7 @@ class MessageComposerViewModel @AssistedInject constructor(
copy(sendMode = SendMode.Regular(currentComposerText, action.fromSharing)) copy(sendMode = SendMode.Regular(currentComposerText, action.fromSharing))
} }
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) { private fun handleEnterEditMode(room: Room, action: MessageComposerAction.EnterEditMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent -> room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
val formatted = vectorPreferences.isRichTextEditorEnabled() val formatted = vectorPreferences.isRichTextEditorEnabled()
setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent(formatted))) } setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent(formatted))) }
@ -168,7 +174,7 @@ class MessageComposerViewModel @AssistedInject constructor(
setState { copy(isFullScreen = action.isFullScreen) } setState { copy(isFullScreen = action.isFullScreen) }
} }
private fun observePowerLevelAndEncryption() { private fun observePowerLevelAndEncryption(room: Room) {
combine( combine(
PowerLevelsFlowFactory(room).createFlow(), PowerLevelsFlowFactory(room).createFlow(),
room.flow().liveRoomSummary().unwrap() room.flow().liveRoomSummary().unwrap()
@ -194,7 +200,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun observeVoiceBroadcast() { private fun observeVoiceBroadcast(room: Room) {
room.stateService().getStateEventLive(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(session.myUserId)) room.stateService().getStateEventLive(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(session.myUserId))
.asFlow() .asFlow()
.unwrap() .unwrap()
@ -204,19 +210,19 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) { private fun handleEnterQuoteMode(room: Room, action: MessageComposerAction.EnterQuoteMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent -> room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Quote(timelineEvent, currentComposerText)) } setState { copy(sendMode = SendMode.Quote(timelineEvent, currentComposerText)) }
} }
} }
private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) { private fun handleEnterReplyMode(room: Room, action: MessageComposerAction.EnterReplyMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent -> room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Reply(timelineEvent, currentComposerText)) } setState { copy(sendMode = SendMode.Reply(timelineEvent, currentComposerText)) }
} }
} }
private fun handleSendMessage(action: MessageComposerAction.SendMessage) { private fun handleSendMessage(room: Room, action: MessageComposerAction.SendMessage) {
withState { state -> withState { state ->
analyticsTracker.capture(state.toAnalyticsComposer()).also { analyticsTracker.capture(state.toAnalyticsComposer()).also {
setState { copy(startsThread = false) } setState { copy(startsThread = false) }
@ -246,7 +252,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
_viewEvents.post(MessageComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft(room)
} }
is ParsedCommand.ErrorSyntax -> { is ParsedCommand.ErrorSyntax -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandError(parsedCommand.command)) _viewEvents.post(MessageComposerViewEvents.SlashCommandError(parsedCommand.command))
@ -272,7 +278,7 @@ class MessageComposerViewModel @AssistedInject constructor(
room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false) room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false)
} }
_viewEvents.post(MessageComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft(room)
} }
is ParsedCommand.SendFormattedText -> { is ParsedCommand.SendFormattedText -> {
// Send the text message to the room, without markdown // Send the text message to the room, without markdown
@ -290,23 +296,23 @@ class MessageComposerViewModel @AssistedInject constructor(
) )
} }
_viewEvents.post(MessageComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft(room)
} }
is ParsedCommand.ChangeRoomName -> { is ParsedCommand.ChangeRoomName -> {
handleChangeRoomNameSlashCommand(parsedCommand) handleChangeRoomNameSlashCommand(room, parsedCommand)
} }
is ParsedCommand.Invite -> { is ParsedCommand.Invite -> {
handleInviteSlashCommand(parsedCommand) handleInviteSlashCommand(room, parsedCommand)
} }
is ParsedCommand.Invite3Pid -> { is ParsedCommand.Invite3Pid -> {
handleInvite3pidSlashCommand(parsedCommand) handleInvite3pidSlashCommand(room, parsedCommand)
} }
is ParsedCommand.SetUserPowerLevel -> { is ParsedCommand.SetUserPowerLevel -> {
handleSetUserPowerLevel(parsedCommand) handleSetUserPowerLevel(room, parsedCommand)
} }
is ParsedCommand.DevTools -> { is ParsedCommand.DevTools -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.ClearScalarToken -> { is ParsedCommand.ClearScalarToken -> {
// TODO // TODO
@ -315,29 +321,29 @@ class MessageComposerViewModel @AssistedInject constructor(
is ParsedCommand.SetMarkdown -> { is ParsedCommand.SetMarkdown -> {
vectorPreferences.setMarkdownEnabled(parsedCommand.enable) vectorPreferences.setMarkdownEnabled(parsedCommand.enable)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.BanUser -> { is ParsedCommand.BanUser -> {
handleBanSlashCommand(parsedCommand) handleBanSlashCommand(room, parsedCommand)
} }
is ParsedCommand.UnbanUser -> { is ParsedCommand.UnbanUser -> {
handleUnbanSlashCommand(parsedCommand) handleUnbanSlashCommand(room, parsedCommand)
} }
is ParsedCommand.IgnoreUser -> { is ParsedCommand.IgnoreUser -> {
handleIgnoreSlashCommand(parsedCommand) handleIgnoreSlashCommand(room, parsedCommand)
} }
is ParsedCommand.UnignoreUser -> { is ParsedCommand.UnignoreUser -> {
handleUnignoreSlashCommand(parsedCommand) handleUnignoreSlashCommand(parsedCommand)
} }
is ParsedCommand.RemoveUser -> { is ParsedCommand.RemoveUser -> {
handleRemoveSlashCommand(parsedCommand) handleRemoveSlashCommand(room, parsedCommand)
} }
is ParsedCommand.JoinRoom -> { is ParsedCommand.JoinRoom -> {
handleJoinToAnotherRoomSlashCommand(parsedCommand) handleJoinToAnotherRoomSlashCommand(parsedCommand)
popDraft() popDraft(room)
} }
is ParsedCommand.PartRoom -> { is ParsedCommand.PartRoom -> {
handlePartSlashCommand(parsedCommand) handlePartSlashCommand(room, parsedCommand)
} }
is ParsedCommand.SendEmote -> { is ParsedCommand.SendEmote -> {
if (state.rootThreadEventId != null) { if (state.rootThreadEventId != null) {
@ -355,7 +361,7 @@ class MessageComposerViewModel @AssistedInject constructor(
) )
} }
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.SendRainbow -> { is ParsedCommand.SendRainbow -> {
val message = parsedCommand.message.toString() val message = parsedCommand.message.toString()
@ -369,7 +375,7 @@ class MessageComposerViewModel @AssistedInject constructor(
room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message)) room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message))
} }
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.SendRainbowEmote -> { is ParsedCommand.SendRainbowEmote -> {
val message = parsedCommand.message.toString() val message = parsedCommand.message.toString()
@ -385,7 +391,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.SendSpoiler -> { is ParsedCommand.SendSpoiler -> {
val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})" val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})"
@ -403,53 +409,53 @@ class MessageComposerViewModel @AssistedInject constructor(
) )
} }
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.SendShrug -> { is ParsedCommand.SendShrug -> {
sendPrefixedMessage("¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId) sendPrefixedMessage(room, "¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.SendLenny -> { is ParsedCommand.SendLenny -> {
sendPrefixedMessage("( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId) sendPrefixedMessage(room, "( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.SendTableFlip -> { is ParsedCommand.SendTableFlip -> {
sendPrefixedMessage("(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId) sendPrefixedMessage(room, "(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.SendChatEffect -> { is ParsedCommand.SendChatEffect -> {
sendChatEffect(parsedCommand) sendChatEffect(room, parsedCommand)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
is ParsedCommand.ChangeTopic -> { is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(parsedCommand) handleChangeTopicSlashCommand(room, parsedCommand)
} }
is ParsedCommand.ChangeDisplayName -> { is ParsedCommand.ChangeDisplayName -> {
handleChangeDisplayNameSlashCommand(parsedCommand) handleChangeDisplayNameSlashCommand(room, parsedCommand)
} }
is ParsedCommand.ChangeDisplayNameForRoom -> { is ParsedCommand.ChangeDisplayNameForRoom -> {
handleChangeDisplayNameForRoomSlashCommand(parsedCommand) handleChangeDisplayNameForRoomSlashCommand(room, parsedCommand)
} }
is ParsedCommand.ChangeRoomAvatar -> { is ParsedCommand.ChangeRoomAvatar -> {
handleChangeRoomAvatarSlashCommand(parsedCommand) handleChangeRoomAvatarSlashCommand(room, parsedCommand)
} }
is ParsedCommand.ChangeAvatarForRoom -> { is ParsedCommand.ChangeAvatarForRoom -> {
handleChangeAvatarForRoomSlashCommand(parsedCommand) handleChangeAvatarForRoomSlashCommand(room, parsedCommand)
} }
is ParsedCommand.ShowUser -> { is ParsedCommand.ShowUser -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
handleWhoisSlashCommand(parsedCommand) handleWhoisSlashCommand(parsedCommand)
popDraft() popDraft(room)
} }
is ParsedCommand.DiscardSession -> { is ParsedCommand.DiscardSession -> {
if (room.roomCryptoService().isEncrypted()) { if (room.roomCryptoService().isEncrypted()) {
session.cryptoService().discardOutboundSession(room.roomId) session.cryptoService().discardOutboundSession(room.roomId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} else { } else {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
_viewEvents.post( _viewEvents.post(
@ -474,7 +480,7 @@ class MessageComposerViewModel @AssistedInject constructor(
null, null,
true true
) )
popDraft() popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -493,7 +499,7 @@ class MessageComposerViewModel @AssistedInject constructor(
null, null,
false false
) )
popDraft() popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -506,7 +512,7 @@ class MessageComposerViewModel @AssistedInject constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias) session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias)
popDraft() popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -518,7 +524,7 @@ class MessageComposerViewModel @AssistedInject constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
session.roomService().leaveRoom(parsedCommand.roomId) session.roomService().leaveRoom(parsedCommand.roomId)
popDraft() popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -534,7 +540,7 @@ class MessageComposerViewModel @AssistedInject constructor(
) )
) )
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft() popDraft(room)
} }
} }
} }
@ -583,7 +589,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
_viewEvents.post(MessageComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft(room)
} }
is SendMode.Quote -> { is SendMode.Quote -> {
room.sendService().sendQuotedTextMessage( room.sendService().sendQuotedTextMessage(
@ -594,7 +600,7 @@ class MessageComposerViewModel @AssistedInject constructor(
rootThreadEventId = state.rootThreadEventId rootThreadEventId = state.rootThreadEventId
) )
_viewEvents.post(MessageComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft(room)
} }
is SendMode.Reply -> { is SendMode.Reply -> {
val timelineEvent = state.sendMode.timelineEvent val timelineEvent = state.sendMode.timelineEvent
@ -619,7 +625,7 @@ class MessageComposerViewModel @AssistedInject constructor(
) )
_viewEvents.post(MessageComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft(room)
} }
is SendMode.Voice -> { is SendMode.Voice -> {
// do nothing // do nothing
@ -628,10 +634,10 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun popDraft() = withState { private fun popDraft(room: Room) = withState {
if (it.sendMode is SendMode.Regular && it.sendMode.fromSharing) { if (it.sendMode is SendMode.Regular && it.sendMode.fromSharing) {
// If we were sharing, we want to get back our last value from draft // If we were sharing, we want to get back our last value from draft
loadDraftIfAny() loadDraftIfAny(room)
} else { } else {
// Otherwise we clear the composer and remove the draft from db // Otherwise we clear the composer and remove the draft from db
setState { copy(sendMode = SendMode.Regular("", false)) } setState { copy(sendMode = SendMode.Regular("", false)) }
@ -641,7 +647,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun loadDraftIfAny() { private fun loadDraftIfAny(room: Room) {
val currentDraft = room.draftService().getDraft() val currentDraft = room.draftService().getDraft()
setState { setState {
copy( copy(
@ -670,7 +676,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) { private fun handleUserIsTyping(room: Room, action: MessageComposerAction.UserIsTyping) {
if (vectorPreferences.sendTypingNotifs()) { if (vectorPreferences.sendTypingNotifs()) {
if (action.isTyping) { if (action.isTyping) {
room.typingService().userIsTyping() room.typingService().userIsTyping()
@ -680,7 +686,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { private fun sendChatEffect(room: Room, sendChatEffect: ParsedCommand.SendChatEffect) {
// If message is blank, convert to an emote, with default message // If message is blank, convert to an emote, with default message
if (sendChatEffect.message.isBlank()) { if (sendChatEffect.message.isBlank()) {
val defaultMessage = stringProvider.getString( val defaultMessage = stringProvider.getString(
@ -732,25 +738,25 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { private fun handleChangeTopicSlashCommand(room: Room, changeTopic: ParsedCommand.ChangeTopic) {
launchSlashCommandFlowSuspendable(changeTopic) { launchSlashCommandFlowSuspendable(room, changeTopic) {
room.stateService().updateTopic(changeTopic.topic) room.stateService().updateTopic(changeTopic.topic)
} }
} }
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { private fun handleInviteSlashCommand(room: Room, invite: ParsedCommand.Invite) {
launchSlashCommandFlowSuspendable(invite) { launchSlashCommandFlowSuspendable(room, invite) {
room.membershipService().invite(invite.userId, invite.reason) room.membershipService().invite(invite.userId, invite.reason)
} }
} }
private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { private fun handleInvite3pidSlashCommand(room: Room, invite: ParsedCommand.Invite3Pid) {
launchSlashCommandFlowSuspendable(invite) { launchSlashCommandFlowSuspendable(room, invite) {
room.membershipService().invite3pid(invite.threePid) room.membershipService().invite3pid(invite.threePid)
} }
} }
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { private fun handleSetUserPowerLevel(room: Room, setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content ?.content
?.toModel<PowerLevelsContent>() ?.toModel<PowerLevelsContent>()
@ -758,19 +764,19 @@ class MessageComposerViewModel @AssistedInject constructor(
?.toContent() ?.toContent()
?: return ?: return
launchSlashCommandFlowSuspendable(setUserPowerLevel) { launchSlashCommandFlowSuspendable(room, setUserPowerLevel) {
room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent) room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent)
} }
} }
private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { private fun handleChangeDisplayNameSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayName) {
launchSlashCommandFlowSuspendable(changeDisplayName) { launchSlashCommandFlowSuspendable(room, changeDisplayName) {
session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName) session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName)
} }
} }
private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) { private fun handlePartSlashCommand(room: Room, command: ParsedCommand.PartRoom) {
launchSlashCommandFlowSuspendable(command) { launchSlashCommandFlowSuspendable(room, command) {
if (command.roomAlias == null) { if (command.roomAlias == null) {
// Leave the current room // Leave the current room
room room
@ -785,39 +791,39 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) { private fun handleRemoveSlashCommand(room: Room, removeUser: ParsedCommand.RemoveUser) {
launchSlashCommandFlowSuspendable(removeUser) { launchSlashCommandFlowSuspendable(room, removeUser) {
room.membershipService().remove(removeUser.userId, removeUser.reason) room.membershipService().remove(removeUser.userId, removeUser.reason)
} }
} }
private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { private fun handleBanSlashCommand(room: Room, ban: ParsedCommand.BanUser) {
launchSlashCommandFlowSuspendable(ban) { launchSlashCommandFlowSuspendable(room, ban) {
room.membershipService().ban(ban.userId, ban.reason) room.membershipService().ban(ban.userId, ban.reason)
} }
} }
private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { private fun handleUnbanSlashCommand(room: Room, unban: ParsedCommand.UnbanUser) {
launchSlashCommandFlowSuspendable(unban) { launchSlashCommandFlowSuspendable(room, unban) {
room.membershipService().unban(unban.userId, unban.reason) room.membershipService().unban(unban.userId, unban.reason)
} }
} }
private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) { private fun handleChangeRoomNameSlashCommand(room: Room, changeRoomName: ParsedCommand.ChangeRoomName) {
launchSlashCommandFlowSuspendable(changeRoomName) { launchSlashCommandFlowSuspendable(room, changeRoomName) {
room.stateService().updateName(changeRoomName.name) room.stateService().updateName(changeRoomName.name)
} }
} }
private fun getMyRoomMemberContent(): RoomMemberContent? { private fun getMyRoomMemberContent(room: Room): RoomMemberContent? {
return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId))
?.content ?.content
?.toModel<RoomMemberContent>() ?.toModel<RoomMemberContent>()
} }
private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { private fun handleChangeDisplayNameForRoomSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) {
launchSlashCommandFlowSuspendable(changeDisplayName) { launchSlashCommandFlowSuspendable(room, changeDisplayName) {
getMyRoomMemberContent() getMyRoomMemberContent(room)
?.copy(displayName = changeDisplayName.displayName) ?.copy(displayName = changeDisplayName.displayName)
?.toContent() ?.toContent()
?.let { ?.let {
@ -826,15 +832,15 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) { private fun handleChangeRoomAvatarSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeRoomAvatar) {
launchSlashCommandFlowSuspendable(changeAvatar) { launchSlashCommandFlowSuspendable(room, changeAvatar) {
room.stateService().sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent()) room.stateService().sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent())
} }
} }
private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) { private fun handleChangeAvatarForRoomSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeAvatarForRoom) {
launchSlashCommandFlowSuspendable(changeAvatar) { launchSlashCommandFlowSuspendable(room, changeAvatar) {
getMyRoomMemberContent() getMyRoomMemberContent(room)
?.copy(avatarUrl = changeAvatar.url) ?.copy(avatarUrl = changeAvatar.url)
?.toContent() ?.toContent()
?.let { ?.let {
@ -843,8 +849,8 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) { private fun handleIgnoreSlashCommand(room: Room, ignore: ParsedCommand.IgnoreUser) {
launchSlashCommandFlowSuspendable(ignore) { launchSlashCommandFlowSuspendable(room, ignore) {
session.userService().ignoreUserIds(listOf(ignore.userId)) session.userService().ignoreUserIds(listOf(ignore.userId))
} }
} }
@ -853,15 +859,15 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore)) _viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore))
} }
private fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) { private fun handleSlashCommandConfirmed(room: Room, action: MessageComposerAction.SlashCommandConfirmed) {
when (action.parsedCommand) { when (action.parsedCommand) {
is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand) is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(room, action.parsedCommand)
else -> TODO("Not handled yet") else -> TODO("Not handled yet")
} }
} }
private fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) { private fun handleUnignoreSlashCommandConfirmed(room: Room, unignore: ParsedCommand.UnignoreUser) {
launchSlashCommandFlowSuspendable(unignore) { launchSlashCommandFlowSuspendable(room, unignore) {
session.userService().unIgnoreUserIds(listOf(unignore.userId)) session.userService().unIgnoreUserIds(listOf(unignore.userId))
} }
} }
@ -870,7 +876,7 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId)) _viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId))
} }
private fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) { private fun sendPrefixedMessage(room: Room, prefix: String, message: CharSequence, rootThreadEventId: String?) {
val sequence = buildString { val sequence = buildString {
append(prefix) append(prefix)
if (message.isNotEmpty()) { if (message.isNotEmpty()) {
@ -886,7 +892,7 @@ class MessageComposerViewModel @AssistedInject constructor(
/** /**
* Convert a send mode to a draft and save the draft. * Convert a send mode to a draft and save the draft.
*/ */
private fun handleSaveTextDraft(draft: String) = withState { private fun handleSaveTextDraft(room: Room, draft: String) = withState {
session.coroutineScope.launch { session.coroutineScope.launch {
when { when {
it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> { it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> {
@ -909,7 +915,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleStartRecordingVoiceMessage() { private fun handleStartRecordingVoiceMessage(room: Room) {
try { try {
audioMessageHelper.startRecording(room.roomId) audioMessageHelper.startRecording(room.roomId)
} catch (failure: Throwable) { } catch (failure: Throwable) {
@ -917,7 +923,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) { private fun handleEndRecordingVoiceMessage(room: Room, isCancelled: Boolean, rootThreadEventId: String? = null) {
audioMessageHelper.stopPlayback() audioMessageHelper.stopPlayback()
if (isCancelled) { if (isCancelled) {
audioMessageHelper.deleteRecording() audioMessageHelper.deleteRecording()
@ -964,7 +970,7 @@ class MessageComposerViewModel @AssistedInject constructor(
audioMessageHelper.stopAllVoiceActions(deleteRecord) audioMessageHelper.stopAllVoiceActions(deleteRecord)
} }
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) { private fun handleInitializeVoiceRecorder(room: Room, attachmentData: ContentAttachmentData) {
audioMessageHelper.initializeRecorder(room.roomId, attachmentData) audioMessageHelper.initializeRecorder(room.roomId, attachmentData)
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) } setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
} }
@ -985,7 +991,7 @@ class MessageComposerViewModel @AssistedInject constructor(
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration) audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
} }
private fun handleEntersBackground(composerText: String) { private fun handleEntersBackground(room: Room, composerText: String) {
// Always stop all voice actions. It may be playing in timeline or active recording // Always stop all voice actions. It may be playing in timeline or active recording
val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false) val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
// TODO remove this when there will be a listening indicator outside of the timeline // TODO remove this when there will be a listening indicator outside of the timeline
@ -1001,7 +1007,7 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
} else { } else {
handleSaveTextDraft(draft = composerText) handleSaveTextDraft(room = room, draft = composerText)
} }
} }
@ -1009,12 +1015,12 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.InsertUserDisplayName(action.userId)) _viewEvents.post(MessageComposerViewEvents.InsertUserDisplayName(action.userId))
} }
private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) { private fun launchSlashCommandFlowSuspendable(room: Room, parsedCommand: ParsedCommand, block: suspend () -> Unit) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch { viewModelScope.launch {
val event = try { val event = try {
block() block()
popDraft() popDraft(room)
MessageComposerViewEvents.SlashCommandResultOk(parsedCommand) MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)
} catch (failure: Throwable) { } catch (failure: Throwable) {
MessageComposerViewEvents.SlashCommandResultError(failure) MessageComposerViewEvents.SlashCommandResultError(failure)
@ -1023,6 +1029,10 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun onRoomError() = setState {
copy(isRoomError = true)
}
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<MessageComposerViewModel, MessageComposerViewState> { interface Factory : MavericksAssistedViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
override fun create(initialState: MessageComposerViewState): MessageComposerViewModel override fun create(initialState: MessageComposerViewState): MessageComposerViewModel

View File

@ -62,6 +62,7 @@ fun CanSendStatus.boolean(): Boolean {
data class MessageComposerViewState( data class MessageComposerViewState(
val roomId: String, val roomId: String,
val isRoomError: Boolean = false,
val canSendMessage: CanSendStatus = CanSendStatus.Allowed, val canSendMessage: CanSendStatus = CanSendStatus.Allowed,
val isSendButtonVisible: Boolean = false, val isSendButtonVisible: Boolean = false,
val rootThreadEventId: String? = null, val rootThreadEventId: String? = null,
@ -88,8 +89,8 @@ data class MessageComposerViewState(
val isVoiceMessageIdle = !isVoiceRecording val isVoiceMessageIdle = !isVoiceRecording
val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording && !isRoomError
val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible && !isRoomError
constructor(args: TimelineArgs) : this( constructor(args: TimelineArgs) : this(
roomId = args.roomId, roomId = args.roomId,