diff --git a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt index 05b70def3d..0eb9dcdaf9 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt @@ -18,6 +18,7 @@ package im.vector.app.core.extensions import android.text.Editable import android.text.InputType +import android.text.Spanned import android.view.MotionEvent import android.view.View import android.view.inputmethod.EditorInfo @@ -57,3 +58,38 @@ fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_searc return@OnTouchListener false }) } + +fun EditText.setTextIfDifferent(newText: CharSequence?): Boolean { + if (!isTextDifferent(newText, text)) { + // Previous text is the same. No op + return false + } + setText(newText) + // Since the text changed we move the cursor to the end of the new text. + // This allows us to fill in text programmatically with a different value, + // but if the user is typing and the view is rebound we won't lose their cursor position. + setSelection(newText?.length ?: 0) + return true +} + +private fun isTextDifferent(str1: CharSequence?, str2: CharSequence?): Boolean { + if (str1 === str2) { + return false + } + if (str1 == null || str2 == null) { + return true + } + val length = str1.length + if (length != str2.length) { + return true + } + if (str1 is Spanned) { + return str1 != str2 + } + for (i in 0 until length) { + if (str1[i] != str2[i]) { + return true + } + } + return false +} 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 520c0a3784..973dca0496 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 @@ -133,6 +133,7 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.composer.SendMode import im.vector.app.features.home.room.detail.composer.TextComposerAction import im.vector.app.features.home.room.detail.composer.TextComposerView import im.vector.app.features.home.room.detail.composer.TextComposerViewEvents @@ -191,7 +192,6 @@ import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.toModel @@ -381,11 +381,11 @@ class RoomDetailFragment @Inject constructor( invalidateOptionsMenu() } - roomDetailViewModel.selectSubscribe(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ -> + roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ -> updateJumpToReadMarkerViewVisibility() } - textComposerViewModel.selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend -> + textComposerViewModel.selectSubscribe(this, TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend -> if (!canSend) { return@selectSubscribe } @@ -397,8 +397,8 @@ class RoomDetailFragment @Inject constructor( } } - roomDetailViewModel.selectSubscribe( + this, RoomDetailViewState::syncState, RoomDetailViewState::incrementalSyncStatus, RoomDetailViewState::pushCounter @@ -412,11 +412,12 @@ class RoomDetailFragment @Inject constructor( } textComposerViewModel.observeViewEvents { - when(it){ - is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) - is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it) - is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message) - is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) + when (it) { + is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) + is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it) + is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message) + is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) + is TextComposerViewEvents.OnSendButtonVisibilityChanged -> handleOnSendButtonVisibilityChanged(it) }.exhaustive } @@ -467,6 +468,21 @@ class RoomDetailFragment @Inject constructor( } } + private fun handleOnSendButtonVisibilityChanged(event: TextComposerViewEvents.OnSendButtonVisibilityChanged) { + Timber.v("Handle on SendButtonVisibility: $event") + if (event.isVisible) { + views.voiceMessageRecorderView.isVisible = false + views.composerLayout.views.sendButton.alpha = 0f + views.composerLayout.views.sendButton.isVisible = true + views.composerLayout.views.sendButton.animate().alpha(1f).setDuration(150).start() + } else { + views.composerLayout.views.sendButton.isInvisible = true + views.voiceMessageRecorderView.alpha = 0f + views.voiceMessageRecorderView.isVisible = true + views.voiceMessageRecorderView.animate().alpha(1f).setDuration(150).start() + } + } + private fun setupRemoveJitsiWidgetView() { views.removeJitsiWidgetView.onCompleteSliding = { withState(roomDetailViewModel) { @@ -669,8 +685,8 @@ class RoomDetailFragment @Inject constructor( views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback { override fun onVoiceRecordingStarted(): Boolean { return if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) { - views.composerLayout.isInvisible = true roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage) + textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(true)) vibrate(requireContext()) true } else { @@ -680,8 +696,8 @@ class RoomDetailFragment @Inject constructor( } override fun onVoiceRecordingEnded(isCancelled: Boolean) { - views.composerLayout.isInvisible = false roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled)) + textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(false)) } override fun onVoiceRecordingPlaybackModeOn() { @@ -767,7 +783,7 @@ class RoomDetailFragment @Inject constructor( } private fun handleJoinedToAnotherRoom(action: TextComposerViewEvents.JoinRoomCommandSuccess) { - updateComposerText("") + views.composerLayout.setTextIfDifferent("") lockSendButton = false navigator.openRoom(vectorBaseActivity, action.roomId) } @@ -993,8 +1009,7 @@ class RoomDetailFragment @Inject constructor( private fun renderRegularMode(text: String) { autoCompleter.exitSpecialMode() views.composerLayout.collapse() - views.voiceMessageRecorderView.isVisible = text.isBlank() - updateComposerText(text) + views.composerLayout.setTextIfDifferent(text) views.composerLayout.views.sendButton.contentDescription = getString(R.string.send) } @@ -1033,7 +1048,7 @@ class RoomDetailFragment @Inject constructor( false } - updateComposerText(defaultContent) + views.composerLayout.setTextIfDifferent(defaultContent) views.composerLayout.views.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) views.composerLayout.views.sendButton.contentDescription = getString(descriptionRes) @@ -1045,21 +1060,11 @@ class RoomDetailFragment @Inject constructor( // need to do it here also when not using quick reply focusComposerAndShowKeyboard() views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible - views.voiceMessageRecorderView.isVisible = false } } focusComposerAndShowKeyboard() } - private fun updateComposerText(text: String) { - // Do not update if this is the same text to avoid the cursor to move - if (text != views.composerLayout.text.toString()) { - // Ignore update to avoid saving a draft - views.composerLayout.views.composerEditText.setText(text) - views.composerLayout.views.composerEditText.setSelection(views.composerLayout.text?.length ?: 0) - } - } - override fun onResume() { super.onResume() notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) @@ -1321,17 +1326,9 @@ class RoomDetailFragment @Inject constructor( return sendUri(contentUri) } - override fun onTextBlankStateChanged(isBlank: Boolean) { - if (!views.composerLayout.views.sendButton.isVisible) { - // Animate alpha to prevent overlapping with the animation of the send button - views.voiceMessageRecorderView.alpha = 0f - views.voiceMessageRecorderView.isVisible = true - views.voiceMessageRecorderView.animate().alpha(1f).setDuration(300).start() - } else { - views.voiceMessageRecorderView.isVisible = false - } + override fun onTextChanged(text: CharSequence) { + textComposerViewModel.handle(TextComposerAction.OnTextChanged(text)) } - } } @@ -1393,16 +1390,14 @@ class RoomDetailFragment @Inject constructor( timelineEventController.update(mainState) lazyLoadedViews.inviteView(false)?.isVisible = false if (mainState.tombstoneEvent == null) { + views.composerLayout.isInvisible = !textComposerState.isComposerVisible + views.voiceMessageRecorderView.isVisible = !textComposerState.isSendButtonVisible + views.composerLayout.views.sendButton.isInvisible = !textComposerState.isSendButtonVisible + views.composerLayout.setRoomEncrypted(summary.isEncrypted) + //views.composerLayout.alwaysShowSendButton = false if (textComposerState.canSendMessage) { - if (!views.voiceMessageRecorderView.isActive()) { - views.composerLayout.isVisible = true - views.voiceMessageRecorderView.isVisible = views.composerLayout.text?.isBlank().orFalse() - views.composerLayout.setRoomEncrypted(summary.isEncrypted) - views.notificationAreaView.render(NotificationAreaView.State.Hidden) - views.composerLayout.alwaysShowSendButton = false - } + views.notificationAreaView.render(NotificationAreaView.State.Hidden) } else { - views.hideComposerViews() views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost) } } else { @@ -1468,7 +1463,7 @@ class RoomDetailFragment @Inject constructor( displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } is TextComposerViewEvents.SlashCommandResultOk -> { - updateComposerText("") + views.composerLayout.setTextIfDifferent("") } is TextComposerViewEvents.SlashCommandResultError -> { displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index a1eac83fc8..d63ba0b973 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -30,26 +30,6 @@ import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType -/** - * Describes the current send mode: - * REGULAR: sends the text as a regular message - * QUOTE: User is currently quoting a message - * EDIT: User is currently editing an existing message - * - * Depending on the state the bottom toolbar will change (icons/preview/actions...) - */ -sealed class SendMode(open val text: String) { - data class REGULAR( - override val text: String, - val fromSharing: Boolean, - // This is necessary for forcing refresh on selectSubscribe - private val ts: Long = System.currentTimeMillis() - ) : SendMode(text) - - data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) - data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) - data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) -} sealed class UnreadState { object Unknown : UnreadState() @@ -74,7 +54,6 @@ data class RoomDetailViewState( val asyncRoomSummary: Async = Uninitialized, val activeRoomWidgets: Async> = Uninitialized, val formattedTypingUsers: String? = null, - val sendMode: SendMode = SendMode.REGULAR("", false), val tombstoneEvent: Event? = null, val joinUpgradedRoomAsync: Async = Uninitialized, val syncState: SyncState = SyncState.Idle, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt index 79ff7be441..48fa13e139 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt @@ -37,11 +37,10 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib interface Callback { fun onRichContentSelected(contentUri: Uri): Boolean - fun onTextBlankStateChanged(isBlank: Boolean) + fun onTextChanged(text: CharSequence) } var callback: Callback? = null - private var isBlankText = true override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { val ic = super.onCreateInputConnection(editorInfo) ?: return null @@ -95,11 +94,7 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib } spanToRemove = null } - // Report blank status of EditText to be able to arrange other elements of the composer - if (s.isBlank() != isBlankText) { - isBlankText = !isBlankText - callback?.onTextBlankStateChanged(isBlankText) - } + callback?.onTextChanged(s.toString()) } } ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt index 7896a009c1..e51d9219e6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt @@ -27,4 +27,6 @@ sealed class TextComposerAction : VectorViewModelAction { data class EnterReplyMode(val eventId: String, val text: String) : TextComposerAction() data class EnterRegularMode(val text: String, val fromSharing: Boolean) : TextComposerAction() data class UserIsTyping(val isTyping: Boolean) : TextComposerAction() + data class OnTextChanged(val text: CharSequence) : TextComposerAction() + data class OnVoiceRecordingStateChanged(val isRecording: Boolean) : TextComposerAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt index eb935f9e75..a033e7366e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt @@ -25,16 +25,14 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable import androidx.core.view.isInvisible -import androidx.core.view.isVisible -import androidx.transition.AutoTransition import androidx.transition.ChangeBounds import androidx.transition.Fade import androidx.transition.Transition import androidx.transition.TransitionManager import androidx.transition.TransitionSet import im.vector.app.R +import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.databinding.ComposerLayoutBinding -import org.matrix.android.sdk.api.extensions.orFalse /** * Encapsulate the timeline composer UX. @@ -61,13 +59,6 @@ class TextComposerView @JvmOverloads constructor( val text: Editable? get() = views.composerEditText.text - var alwaysShowSendButton = false - set(value) { - field = value - val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || text?.isNotBlank().orFalse() || value - views.sendButton.isInvisible = !shouldShowSendButton - } - init { inflate(context, R.layout.composer_layout, this) views = ComposerLayoutBinding.bind(this) @@ -79,17 +70,8 @@ class TextComposerView @JvmOverloads constructor( return callback?.onRichContentSelected(contentUri) ?: false } - override fun onTextBlankStateChanged(isBlank: Boolean) { - val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isBlank || alwaysShowSendButton - TransitionManager.endTransitions(this@TextComposerView) - if (views.sendButton.isVisible != shouldShowSendButton) { - TransitionManager.beginDelayedTransition( - this@TextComposerView, - AutoTransition().also { it.duration = 150 } - ) - views.sendButton.isInvisible = !shouldShowSendButton - } - callback?.onTextBlankStateChanged(isBlank) + override fun onTextChanged(text: CharSequence) { + callback?.onTextChanged(text) } } views.composerRelatedMessageCloseButton.setOnClickListener { @@ -114,9 +96,6 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) - - val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty() || alwaysShowSendButton - views.sendButton.isInvisible = !shouldShowSendButton } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -126,10 +105,14 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) - views.sendButton.isInvisible = false + } + + fun setTextIfDifferent(text: CharSequence?): Boolean{ + return views.composerEditText.setTextIfDifferent(text) } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { + //val wasSendButtonInvisible = views.sendButton.isInvisible if (animate) { configureAndBeginTransition(transitionComplete) } @@ -137,6 +120,8 @@ class TextComposerView @JvmOverloads constructor( it.clone(context, currentConstraintSetId) it.applyTo(this) } + // Might be updated by view state just after, but avoid blinks + //views.sendButton.isInvisible = wasSendButtonInvisible } private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) { 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 cce33a5b63..8ef1cca2da 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 @@ -22,6 +22,8 @@ import im.vector.app.features.command.Command sealed class TextComposerViewEvents : VectorViewEvents { + data class OnSendButtonVisibilityChanged(val isVisible: Boolean): TextComposerViewEvents() + data class ShowMessage(val message: String) : TextComposerViewEvents() abstract class SendMessageResult : 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 870f5682e0..8d4dd78c29 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 @@ -17,29 +17,20 @@ package im.vector.app.features.home.room.detail.composer import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand -import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.home.room.detail.ChatEffect -import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailFragment -import im.vector.app.features.home.room.detail.SendMode import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.toMessageType import im.vector.app.features.powerlevel.PowerLevelsObservableFactory @@ -74,24 +65,57 @@ class TextComposerViewModel @AssistedInject constructor( private val room = session.getRoom(initialState.roomId)!! + // Keep it out of state to avoid invalidate being called + private var currentComposerText: CharSequence = "" + init { loadDraftIfAny() observePowerLevel() + subscribeToStateInternal() } override fun handle(action: TextComposerAction) { Timber.v("Handle action: $action") when (action) { - is TextComposerAction.EnterEditMode -> handleEnterEditMode(action) - is TextComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) - is TextComposerAction.EnterRegularMode -> handleEnterRegularMode(action) - is TextComposerAction.EnterReplyMode -> handleEnterReplyMode(action) - is TextComposerAction.SaveDraft -> handleSaveDraft(action) - is TextComposerAction.SendMessage -> handleSendMessage(action) - is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) + is TextComposerAction.EnterEditMode -> handleEnterEditMode(action) + is TextComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) + is TextComposerAction.EnterRegularMode -> handleEnterRegularMode(action) + is TextComposerAction.EnterReplyMode -> handleEnterReplyMode(action) + is TextComposerAction.SaveDraft -> handleSaveDraft(action) + is TextComposerAction.SendMessage -> handleSendMessage(action) + is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) + is TextComposerAction.OnTextChanged -> handleOnTextChanged(action) + is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action) } } + private fun handleOnVoiceRecordingStateChanged(action: TextComposerAction.OnVoiceRecordingStateChanged) = setState { + copy(isVoiceRecording = action.isRecording) + } + + private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) { + setState { + // Makes sure currentComposerText is upToDate when accessing further setState + currentComposerText = action.text + this + } + updateIsSendButtonVisibility() + } + + private fun subscribeToStateInternal() { + selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage, TextComposerViewState::isVoiceRecording) { _, _, _ -> + updateIsSendButtonVisibility() + } + } + + private fun updateIsSendButtonVisibility() = setState { + val isSendButtonVisible = isComposerVisible && (sendMode !is SendMode.REGULAR || currentComposerText.isNotBlank()) + if (this.isSendButtonVisible != isSendButtonVisible) { + _viewEvents.post(TextComposerViewEvents.OnSendButtonVisibilityChanged(isSendButtonVisible)) + } + copy(isSendButtonVisible = isSendButtonVisible) + } + private fun handleEnterRegularMode(action: TextComposerAction.EnterRegularMode) = setState { copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing)) } @@ -442,7 +466,6 @@ class TextComposerViewModel @AssistedInject constructor( } } - private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { // If message is blank, convert to an emote, with default message if (sendChatEffect.message.isBlank()) { @@ -573,7 +596,6 @@ class TextComposerViewModel @AssistedInject constructor( } } - private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) viewModelScope.launch { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt index 1e118ef38a..2fe9b58b21 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt @@ -18,16 +18,40 @@ package im.vector.app.features.home.room.detail.composer import com.airbnb.mvrx.MvRxState import im.vector.app.features.home.room.detail.RoomDetailArgs -import im.vector.app.features.home.room.detail.SendMode +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * Describes the current send mode: + * REGULAR: sends the text as a regular message + * QUOTE: User is currently quoting a message + * EDIT: User is currently editing an existing message + * + * Depending on the state the bottom toolbar will change (icons/preview/actions...) + */ +sealed class SendMode(open val text: String) { + data class REGULAR( + override val text: String, + val fromSharing: Boolean, + // This is necessary for forcing refresh on selectSubscribe + private val ts: Long = System.currentTimeMillis() + ) : SendMode(text) + + data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) +} data class TextComposerViewState( val roomId: String, val canSendMessage: Boolean = true, - val isSendButtonVisible: Boolean = false, - val isRecordingVoiceMessage: Boolean = false, + val isVoiceRecording: Boolean = false, + val isSendButtonVisible : Boolean = false, val sendMode: SendMode = SendMode.REGULAR("", false), ) : MvRxState { + val isComposerVisible: Boolean + get() = canSendMessage && !isVoiceRecording + constructor(args: RoomDetailArgs) : this( roomId = args.roomId, ) diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml index eae8457121..f42551d39a 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml @@ -172,7 +172,7 @@ android:contentDescription="@string/send" android:scaleType="center" android:src="@drawable/ic_send" - android:visibility="gone" + android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" tools:ignore="MissingPrefix" diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml index c09b95f6f7..d6a5b57884 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml @@ -184,7 +184,7 @@ android:contentDescription="@string/send" android:scaleType="center" android:src="@drawable/ic_send" - android:visibility="gone" + android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"