mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
TextComposer: continue reworking. WIP
This commit is contained in:
parent
9815dfe449
commit
6b3a407b79
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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<RoomSummary> = Uninitialized,
|
||||
val activeRoomWidgets: Async<List<Widget>> = Uninitialized,
|
||||
val formattedTypingUsers: String? = null,
|
||||
val sendMode: SendMode = SendMode.REGULAR("", false),
|
||||
val tombstoneEvent: Event? = null,
|
||||
val joinUpgradedRoomAsync: Async<String> = Uninitialized,
|
||||
val syncState: SyncState = SyncState.Idle,
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user