mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Merge pull request #4523 from vector-im/feature/adm/voice-composer
Moving voice logic to the MessageComposer
This commit is contained in:
commit
97a44a5632
@ -41,7 +41,7 @@ import im.vector.app.features.home.PromoteRestrictedViewModel
|
|||||||
import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
|
import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
|
||||||
import im.vector.app.features.home.UnreadMessagesSharedViewModel
|
import im.vector.app.features.home.UnreadMessagesSharedViewModel
|
||||||
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
|
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
|
||||||
import im.vector.app.features.home.room.detail.composer.TextComposerViewModel
|
import im.vector.app.features.home.room.detail.RoomDetailViewModel
|
||||||
import im.vector.app.features.home.room.detail.search.SearchViewModel
|
import im.vector.app.features.home.room.detail.search.SearchViewModel
|
||||||
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
|
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
|
||||||
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
|
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
|
||||||
@ -505,8 +505,8 @@ interface MavericksViewModelModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@MavericksViewModelKey(TextComposerViewModel::class)
|
@MavericksViewModelKey(RoomDetailViewModel::class)
|
||||||
fun textComposerViewModelFactory(factory: TextComposerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
fun roomDetailViewModelFactory(factory: RoomDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
|
@ -21,7 +21,6 @@ import android.view.View
|
|||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.call.conference.ConferenceEvent
|
import im.vector.app.features.call.conference.ConferenceEvent
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||||
@ -108,12 +107,4 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||||||
object RemoveAllFailedMessages : RoomDetailAction()
|
object RemoveAllFailedMessages : RoomDetailAction()
|
||||||
|
|
||||||
data class RoomUpgradeSuccess(val replacementRoomId: String) : RoomDetailAction()
|
data class RoomUpgradeSuccess(val replacementRoomId: String) : RoomDetailAction()
|
||||||
|
|
||||||
// Voice Message
|
|
||||||
object StartRecordingVoiceMessage : RoomDetailAction()
|
|
||||||
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : RoomDetailAction()
|
|
||||||
object PauseRecordingVoiceMessage : RoomDetailAction()
|
|
||||||
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : RoomDetailAction()
|
|
||||||
object PlayOrPauseRecordingPlayback : RoomDetailAction()
|
|
||||||
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : RoomDetailAction()
|
|
||||||
}
|
}
|
||||||
|
@ -131,12 +131,12 @@ import im.vector.app.features.command.Command
|
|||||||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
||||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import im.vector.app.features.home.room.detail.composer.MessageComposerAction
|
||||||
|
import im.vector.app.features.home.room.detail.composer.MessageComposerView
|
||||||
|
import im.vector.app.features.home.room.detail.composer.MessageComposerViewEvents
|
||||||
|
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
|
||||||
|
import im.vector.app.features.home.room.detail.composer.MessageComposerViewState
|
||||||
import im.vector.app.features.home.room.detail.composer.SendMode
|
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
|
|
||||||
import im.vector.app.features.home.room.detail.composer.TextComposerViewModel
|
|
||||||
import im.vector.app.features.home.room.detail.composer.TextComposerViewState
|
|
||||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
|
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
|
||||||
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||||
@ -240,7 +240,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
autoCompleterFactory: AutoCompleter.Factory,
|
autoCompleterFactory: AutoCompleter.Factory,
|
||||||
private val permalinkHandler: PermalinkHandler,
|
private val permalinkHandler: PermalinkHandler,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
|
val messageComposerViewModelFactory: MessageComposerViewModel.Factory,
|
||||||
private val eventHtmlRenderer: EventHtmlRenderer,
|
private val eventHtmlRenderer: EventHtmlRenderer,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
@ -293,7 +293,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
autoCompleterFactory.create(roomDetailArgs.roomId)
|
autoCompleterFactory.create(roomDetailArgs.roomId)
|
||||||
}
|
}
|
||||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||||
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
private val messageComposerViewModel: MessageComposerViewModel by fragmentViewModel()
|
||||||
private val debouncer = Debouncer(createUIHandler())
|
private val debouncer = Debouncer(createUIHandler())
|
||||||
|
|
||||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||||
@ -386,7 +386,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
updateJumpToReadMarkerViewVisibility()
|
updateJumpToReadMarkerViewVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
textComposerViewModel.onEach(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend ->
|
messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
|
||||||
if (!canSend) {
|
if (!canSend) {
|
||||||
return@onEach
|
return@onEach
|
||||||
}
|
}
|
||||||
@ -411,25 +411,26 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
textComposerViewModel.observeViewEvents {
|
messageComposerViewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
|
is MessageComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
|
||||||
is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
|
is MessageComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
|
||||||
is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
|
is MessageComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
|
||||||
is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
|
is MessageComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
|
||||||
is TextComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it)
|
is MessageComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it)
|
||||||
is TextComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId)
|
is MessageComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId)
|
||||||
}.exhaustive
|
is MessageComposerViewEvents.VoicePlaybackOrRecordingFailure -> {
|
||||||
}
|
|
||||||
|
|
||||||
roomDetailViewModel.observeViewEvents {
|
|
||||||
when (it) {
|
|
||||||
is RoomDetailViewEvents.Failure -> {
|
|
||||||
if (it.throwable is VoiceFailure.UnableToRecord) {
|
if (it.throwable is VoiceFailure.UnableToRecord) {
|
||||||
onCannotRecord()
|
onCannotRecord()
|
||||||
}
|
}
|
||||||
showErrorInSnackbar(it.throwable)
|
showErrorInSnackbar(it.throwable)
|
||||||
}
|
}
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
roomDetailViewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
||||||
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
||||||
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
|
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
|
||||||
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
|
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
|
||||||
@ -469,7 +470,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSendButtonVisibilityChanged(event: TextComposerViewEvents.AnimateSendButtonVisibility) {
|
private fun handleSendButtonVisibilityChanged(event: MessageComposerViewEvents.AnimateSendButtonVisibility) {
|
||||||
if (event.isVisible) {
|
if (event.isVisible) {
|
||||||
views.voiceMessageRecorderView.isVisible = false
|
views.voiceMessageRecorderView.isVisible = false
|
||||||
views.composerLayout.views.sendButton.alpha = 0f
|
views.composerLayout.views.sendButton.alpha = 0f
|
||||||
@ -505,7 +506,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
|
|
||||||
private fun onCannotRecord() {
|
private fun onCannotRecord() {
|
||||||
// Update the UI, cancel the animation
|
// Update the UI, cancel the animation
|
||||||
textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.None))
|
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.None))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
|
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
|
||||||
@ -524,7 +525,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
JoinReplacementRoomBottomSheet().show(childFragmentManager, tag)
|
JoinReplacementRoomBottomSheet().show(childFragmentManager, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: TextComposerViewEvents.ShowRoomUpgradeDialog) {
|
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: MessageComposerViewEvents.ShowRoomUpgradeDialog) {
|
||||||
val tag = MigrateRoomBottomSheet::javaClass.name
|
val tag = MigrateRoomBottomSheet::javaClass.name
|
||||||
MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion)
|
MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion)
|
||||||
.show(parentFragmentManager, tag)
|
.show(parentFragmentManager, tag)
|
||||||
@ -697,18 +698,18 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
|
|
||||||
override fun onVoiceRecordingStarted() {
|
override fun onVoiceRecordingStarted() {
|
||||||
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage)
|
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
|
||||||
vibrate(requireContext())
|
vibrate(requireContext())
|
||||||
updateRecordingUiState(RecordingUiState.Started)
|
updateRecordingUiState(RecordingUiState.Started)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoicePlaybackButtonClicked() {
|
override fun onVoicePlaybackButtonClicked() {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseRecordingPlayback)
|
messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseRecordingPlayback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoiceRecordingCancelled() {
|
override fun onVoiceRecordingCancelled() {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true))
|
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
|
||||||
updateRecordingUiState(RecordingUiState.Cancelled)
|
updateRecordingUiState(RecordingUiState.Cancelled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -721,27 +722,27 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSendVoiceMessage() {
|
override fun onSendVoiceMessage() {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = false))
|
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false))
|
||||||
updateRecordingUiState(RecordingUiState.None)
|
updateRecordingUiState(RecordingUiState.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteVoiceMessage() {
|
override fun onDeleteVoiceMessage() {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true))
|
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
|
||||||
updateRecordingUiState(RecordingUiState.None)
|
updateRecordingUiState(RecordingUiState.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRecordingLimitReached() {
|
override fun onRecordingLimitReached() {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.PauseRecordingVoiceMessage)
|
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
|
||||||
updateRecordingUiState(RecordingUiState.Playback)
|
updateRecordingUiState(RecordingUiState.Playback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRecordingWaveformClicked() {
|
override fun onRecordingWaveformClicked() {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.PauseRecordingVoiceMessage)
|
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
|
||||||
updateRecordingUiState(RecordingUiState.Playback)
|
updateRecordingUiState(RecordingUiState.Playback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateRecordingUiState(state: RecordingUiState) {
|
private fun updateRecordingUiState(state: RecordingUiState) {
|
||||||
textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingUiStateChanged(state))
|
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(state))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -818,7 +819,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleJoinedToAnotherRoom(action: TextComposerViewEvents.JoinRoomCommandSuccess) {
|
private fun handleJoinedToAnotherRoom(action: MessageComposerViewEvents.JoinRoomCommandSuccess) {
|
||||||
views.composerLayout.setTextIfDifferent("")
|
views.composerLayout.setTextIfDifferent("")
|
||||||
lockSendButton = false
|
lockSendButton = false
|
||||||
navigator.openRoom(vectorBaseActivity, action.roomId)
|
navigator.openRoom(vectorBaseActivity, action.roomId)
|
||||||
@ -827,7 +828,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
private fun handleShareData() {
|
private fun handleShareData() {
|
||||||
when (val sharedData = roomDetailArgs.sharedData) {
|
when (val sharedData = roomDetailArgs.sharedData) {
|
||||||
is SharedData.Text -> {
|
is SharedData.Text -> {
|
||||||
textComposerViewModel.handle(TextComposerAction.EnterRegularMode(sharedData.text, fromSharing = true))
|
messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(sharedData.text, fromSharing = true))
|
||||||
}
|
}
|
||||||
is SharedData.Attachments -> {
|
is SharedData.Attachments -> {
|
||||||
// open share edition
|
// open share edition
|
||||||
@ -1132,11 +1133,11 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
|
|
||||||
notificationDrawerManager.setCurrentRoom(null)
|
notificationDrawerManager.setCurrentRoom(null)
|
||||||
|
|
||||||
textComposerViewModel.handle(TextComposerAction.SaveDraft(views.composerLayout.text.toString()))
|
messageComposerViewModel.handle(MessageComposerAction.SaveDraft(views.composerLayout.text.toString()))
|
||||||
|
|
||||||
// We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
|
// We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
|
||||||
roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions(deleteRecord = false))
|
messageComposerViewModel.handle(MessageComposerAction.EndAllVoiceActions(deleteRecord = false))
|
||||||
views.voiceMessageRecorderView.display(RecordingUiState.None)
|
views.voiceMessageRecorderView.render(RecordingUiState.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
|
private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
|
||||||
@ -1251,12 +1252,12 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||||
(model as? AbsMessageItem)?.attributes?.informationData?.let {
|
(model as? AbsMessageItem)?.attributes?.informationData?.let {
|
||||||
val eventId = it.eventId
|
val eventId = it.eventId
|
||||||
textComposerViewModel.handle(TextComposerAction.EnterReplyMode(eventId, views.composerLayout.text.toString()))
|
messageComposerViewModel.handle(MessageComposerAction.EnterReplyMode(eventId, views.composerLayout.text.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||||
val canSendMessage = withState(textComposerViewModel) {
|
val canSendMessage = withState(messageComposerViewModel) {
|
||||||
it.canSendMessage
|
it.canSendMessage
|
||||||
}
|
}
|
||||||
if (!canSendMessage) {
|
if (!canSendMessage) {
|
||||||
@ -1345,7 +1346,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
|
|
||||||
views.composerLayout.views.composerEmojiButton.isVisible = vectorPreferences.showEmojiKeyboard()
|
views.composerLayout.views.composerEmojiButton.isVisible = vectorPreferences.showEmojiKeyboard()
|
||||||
|
|
||||||
views.composerLayout.callback = object : TextComposerView.Callback {
|
views.composerLayout.callback = object : MessageComposerView.Callback {
|
||||||
override fun onAddAttachment() {
|
override fun onAddAttachment() {
|
||||||
if (!::attachmentTypeSelector.isInitialized) {
|
if (!::attachmentTypeSelector.isInitialized) {
|
||||||
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment)
|
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment)
|
||||||
@ -1358,7 +1359,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCloseRelatedMessage() {
|
override fun onCloseRelatedMessage() {
|
||||||
textComposerViewModel.handle(TextComposerAction.EnterRegularMode(views.composerLayout.text.toString(), false))
|
messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(views.composerLayout.text.toString(), false))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||||
@ -1366,7 +1367,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTextChanged(text: CharSequence) {
|
override fun onTextChanged(text: CharSequence) {
|
||||||
textComposerViewModel.handle(TextComposerAction.OnTextChanged(text))
|
messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1380,7 +1381,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
// We collapse ASAP, if not there will be a slight annoying delay
|
// We collapse ASAP, if not there will be a slight annoying delay
|
||||||
views.composerLayout.collapse(true)
|
views.composerLayout.collapse(true)
|
||||||
lockSendButton = true
|
lockSendButton = true
|
||||||
textComposerViewModel.handle(TextComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
|
messageComposerViewModel.handle(MessageComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
|
||||||
emojiPopup.dismiss()
|
emojiPopup.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1392,7 +1393,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
.map { it.isNotEmpty() }
|
.map { it.isNotEmpty() }
|
||||||
.onEach {
|
.onEach {
|
||||||
Timber.d("Typing: User is typing: $it")
|
Timber.d("Typing: User is typing: $it")
|
||||||
textComposerViewModel.handle(TextComposerAction.UserIsTyping(it))
|
messageComposerViewModel.handle(MessageComposerAction.UserIsTyping(it))
|
||||||
}
|
}
|
||||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
|
||||||
@ -1412,7 +1413,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
return isHandled
|
return isHandled
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(roomDetailViewModel, textComposerViewModel) { mainState, textComposerState ->
|
override fun invalidate() = withState(roomDetailViewModel, messageComposerViewModel) { mainState, messageComposerState ->
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
val summary = mainState.asyncRoomSummary()
|
val summary = mainState.asyncRoomSummary()
|
||||||
renderToolbar(summary, mainState.formattedTypingUsers)
|
renderToolbar(summary, mainState.formattedTypingUsers)
|
||||||
@ -1429,13 +1430,13 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
timelineEventController.update(mainState)
|
timelineEventController.update(mainState)
|
||||||
lazyLoadedViews.inviteView(false)?.isVisible = false
|
lazyLoadedViews.inviteView(false)?.isVisible = false
|
||||||
if (mainState.tombstoneEvent == null) {
|
if (mainState.tombstoneEvent == null) {
|
||||||
views.composerLayout.isInvisible = !textComposerState.isComposerVisible
|
views.composerLayout.isInvisible = !messageComposerState.isComposerVisible
|
||||||
views.voiceMessageRecorderView.isVisible = textComposerState.isVoiceMessageRecorderVisible
|
views.voiceMessageRecorderView.isVisible = messageComposerState.isVoiceMessageRecorderVisible
|
||||||
views.composerLayout.views.sendButton.isInvisible = !textComposerState.isSendButtonVisible
|
views.composerLayout.views.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
|
||||||
views.voiceMessageRecorderView.display(textComposerState.voiceRecordingUiState)
|
views.voiceMessageRecorderView.render(messageComposerState.voiceRecordingUiState)
|
||||||
views.composerLayout.setRoomEncrypted(summary.isEncrypted)
|
views.composerLayout.setRoomEncrypted(summary.isEncrypted)
|
||||||
// views.composerLayout.alwaysShowSendButton = false
|
// views.composerLayout.alwaysShowSendButton = false
|
||||||
if (textComposerState.canSendMessage) {
|
if (messageComposerState.canSendMessage) {
|
||||||
views.notificationAreaView.render(NotificationAreaView.State.Hidden)
|
views.notificationAreaView.render(NotificationAreaView.State.Hidden)
|
||||||
} else {
|
} else {
|
||||||
views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost)
|
views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost)
|
||||||
@ -1492,27 +1493,27 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderSendMessageResult(sendMessageResult: TextComposerViewEvents.SendMessageResult) {
|
private fun renderSendMessageResult(sendMessageResult: MessageComposerViewEvents.SendMessageResult) {
|
||||||
when (sendMessageResult) {
|
when (sendMessageResult) {
|
||||||
is TextComposerViewEvents.SlashCommandLoading -> {
|
is MessageComposerViewEvents.SlashCommandLoading -> {
|
||||||
showLoading(null)
|
showLoading(null)
|
||||||
}
|
}
|
||||||
is TextComposerViewEvents.SlashCommandError -> {
|
is MessageComposerViewEvents.SlashCommandError -> {
|
||||||
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
||||||
}
|
}
|
||||||
is TextComposerViewEvents.SlashCommandUnknown -> {
|
is MessageComposerViewEvents.SlashCommandUnknown -> {
|
||||||
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
|
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
|
||||||
}
|
}
|
||||||
is TextComposerViewEvents.SlashCommandResultOk -> {
|
is MessageComposerViewEvents.SlashCommandResultOk -> {
|
||||||
dismissLoadingDialog()
|
dismissLoadingDialog()
|
||||||
views.composerLayout.setTextIfDifferent("")
|
views.composerLayout.setTextIfDifferent("")
|
||||||
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
|
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
|
||||||
}
|
}
|
||||||
is TextComposerViewEvents.SlashCommandResultError -> {
|
is MessageComposerViewEvents.SlashCommandResultError -> {
|
||||||
dismissLoadingDialog()
|
dismissLoadingDialog()
|
||||||
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
|
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
|
||||||
}
|
}
|
||||||
is TextComposerViewEvents.SlashCommandNotImplemented -> {
|
is MessageComposerViewEvents.SlashCommandNotImplemented -> {
|
||||||
displayCommandError(getString(R.string.not_implemented))
|
displayCommandError(getString(R.string.not_implemented))
|
||||||
}
|
}
|
||||||
} // .exhaustive
|
} // .exhaustive
|
||||||
@ -1883,7 +1884,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent) {
|
override fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent) {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
|
messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
||||||
@ -1988,18 +1989,18 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
|
roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
|
||||||
}
|
}
|
||||||
is EventSharedAction.Edit -> {
|
is EventSharedAction.Edit -> {
|
||||||
if (withState(textComposerViewModel) { it.isVoiceMessageIdle }) {
|
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
|
||||||
textComposerViewModel.handle(TextComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
|
messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
|
||||||
} else {
|
} else {
|
||||||
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is EventSharedAction.Quote -> {
|
is EventSharedAction.Quote -> {
|
||||||
textComposerViewModel.handle(TextComposerAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString()))
|
messageComposerViewModel.handle(MessageComposerAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString()))
|
||||||
}
|
}
|
||||||
is EventSharedAction.Reply -> {
|
is EventSharedAction.Reply -> {
|
||||||
if (withState(textComposerViewModel) { it.isVoiceMessageIdle }) {
|
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
|
||||||
textComposerViewModel.handle(TextComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString()))
|
messageComposerViewModel.handle(MessageComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString()))
|
||||||
} else {
|
} else {
|
||||||
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
||||||
}
|
}
|
||||||
@ -2212,7 +2213,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||||
super.onContactAttachmentReady(contactAttachment)
|
super.onContactAttachmentReady(contactAttachment)
|
||||||
val formattedContact = contactAttachment.toHumanReadable()
|
val formattedContact = contactAttachment.toHumanReadable()
|
||||||
textComposerViewModel.handle(TextComposerAction.SendMessage(formattedContact, false))
|
messageComposerViewModel.handle(MessageComposerAction.SendMessage(formattedContact, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onViewWidgetsClicked() {
|
private fun onViewWidgetsClicked() {
|
||||||
|
@ -21,24 +21,23 @@ import androidx.annotation.IdRes
|
|||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.Fail
|
import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.flow.chunk
|
import im.vector.app.core.flow.chunk
|
||||||
import im.vector.app.core.mvrx.runCatchingToAsync
|
import im.vector.app.core.mvrx.runCatchingToAsync
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.core.utils.BehaviorDataSource
|
import im.vector.app.core.utils.BehaviorDataSource
|
||||||
import im.vector.app.features.attachments.toContentAttachmentData
|
|
||||||
import im.vector.app.features.call.conference.ConferenceEvent
|
import im.vector.app.features.call.conference.ConferenceEvent
|
||||||
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
|
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
|
||||||
import im.vector.app.features.call.conference.JitsiService
|
import im.vector.app.features.call.conference.JitsiService
|
||||||
@ -47,7 +46,6 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
|
|||||||
import im.vector.app.features.createdirect.DirectRoomHelper
|
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||||
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
||||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||||
import im.vector.app.features.home.room.detail.composer.VoiceMessageHelper
|
|
||||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
||||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
||||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
@ -56,7 +54,6 @@ import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
|||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.settings.VectorDataStore
|
import im.vector.app.features.settings.VectorDataStore
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.voice.VoicePlayerHelper
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
@ -116,8 +113,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
private val directRoomHelper: DirectRoomHelper,
|
private val directRoomHelper: DirectRoomHelper,
|
||||||
private val jitsiService: JitsiService,
|
private val jitsiService: JitsiService,
|
||||||
private val activeConferenceHolder: JitsiActiveConferenceHolder,
|
private val activeConferenceHolder: JitsiActiveConferenceHolder,
|
||||||
private val voiceMessageHelper: VoiceMessageHelper,
|
|
||||||
private val voicePlayerHelper: VoicePlayerHelper,
|
|
||||||
timelineFactory: TimelineFactory
|
timelineFactory: TimelineFactory
|
||||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||||
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
|
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
|
||||||
@ -144,22 +139,12 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
private var prepareToEncrypt: Async<Unit> = Uninitialized
|
private var prepareToEncrypt: Async<Unit> = Uninitialized
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory : MavericksAssistedViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
||||||
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
|
override fun create(initialState: RoomDetailViewState): RoomDetailViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
companion object : MavericksViewModelFactory<RoomDetailViewModel, RoomDetailViewState> by hiltMavericksViewModelFactory() {
|
||||||
* Can't use the hiltMaverick here because some dependencies are injected here and in fragment but they don't share the graph.
|
|
||||||
*/
|
|
||||||
companion object : MavericksViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
|
||||||
|
|
||||||
const val PAGINATION_COUNT = 50
|
const val PAGINATION_COUNT = 50
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel {
|
|
||||||
val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
|
||||||
return fragment.roomDetailViewModelFactory.create(state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -343,12 +328,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||||
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
|
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
|
||||||
RoomDetailAction.ResendAll -> handleResendAll()
|
RoomDetailAction.ResendAll -> handleResendAll()
|
||||||
RoomDetailAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
|
|
||||||
is RoomDetailAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled)
|
|
||||||
is RoomDetailAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
|
|
||||||
RoomDetailAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
|
|
||||||
RoomDetailAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
|
|
||||||
is RoomDetailAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
|
|
||||||
is RoomDetailAction.RoomUpgradeSuccess -> {
|
is RoomDetailAction.RoomUpgradeSuccess -> {
|
||||||
setState {
|
setState {
|
||||||
copy(joinUpgradedRoomAsync = Success(action.replacementRoomId))
|
copy(joinUpgradedRoomAsync = Success(action.replacementRoomId))
|
||||||
@ -612,56 +591,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleStartRecordingVoiceMessage() {
|
|
||||||
try {
|
|
||||||
voiceMessageHelper.startRecording()
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
_viewEvents.post(RoomDetailViewEvents.Failure(failure))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) {
|
|
||||||
voiceMessageHelper.stopPlayback()
|
|
||||||
if (isCancelled) {
|
|
||||||
voiceMessageHelper.deleteRecording()
|
|
||||||
} else {
|
|
||||||
voiceMessageHelper.stopRecording()?.let { audioType ->
|
|
||||||
if (audioType.duration > 1000) {
|
|
||||||
room.sendMedia(audioType.toContentAttachmentData(), false, emptySet())
|
|
||||||
} else {
|
|
||||||
voiceMessageHelper.deleteRecording()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePlayOrPauseVoicePlayback(action: RoomDetailAction.PlayOrPauseVoicePlayback) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
// Download can fail
|
|
||||||
val audioFile = session.fileService().downloadFile(action.messageAudioContent)
|
|
||||||
// Conversion can fail, fallback to the original file in this case and let the player fail for us
|
|
||||||
val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile
|
|
||||||
// Play can fail
|
|
||||||
voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
_viewEvents.post(RoomDetailViewEvents.Failure(failure))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePlayOrPauseRecordingPlayback() {
|
|
||||||
voiceMessageHelper.startOrPauseRecordingPlayback()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
|
|
||||||
voiceMessageHelper.stopAllVoiceActions(deleteRecord)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePauseRecordingVoiceMessage() {
|
|
||||||
voiceMessageHelper.pauseRecording()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
|
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
|
||||||
|
|
||||||
fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->
|
fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->
|
||||||
|
@ -18,15 +18,24 @@ package im.vector.app.features.home.room.detail.composer
|
|||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||||
|
|
||||||
sealed class TextComposerAction : VectorViewModelAction {
|
sealed class MessageComposerAction : VectorViewModelAction {
|
||||||
data class SaveDraft(val draft: String) : TextComposerAction()
|
data class SaveDraft(val draft: String) : MessageComposerAction()
|
||||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : TextComposerAction()
|
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
|
||||||
data class EnterEditMode(val eventId: String, val text: String) : TextComposerAction()
|
data class EnterEditMode(val eventId: String, val text: String) : MessageComposerAction()
|
||||||
data class EnterQuoteMode(val eventId: String, val text: String) : TextComposerAction()
|
data class EnterQuoteMode(val eventId: String, val text: String) : MessageComposerAction()
|
||||||
data class EnterReplyMode(val eventId: String, val text: String) : TextComposerAction()
|
data class EnterReplyMode(val eventId: String, val text: String) : MessageComposerAction()
|
||||||
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : TextComposerAction()
|
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : MessageComposerAction()
|
||||||
data class UserIsTyping(val isTyping: Boolean) : TextComposerAction()
|
data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
|
||||||
data class OnTextChanged(val text: CharSequence) : TextComposerAction()
|
data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
|
||||||
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : TextComposerAction()
|
|
||||||
|
// Voice Message
|
||||||
|
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
|
||||||
|
object StartRecordingVoiceMessage : MessageComposerAction()
|
||||||
|
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : MessageComposerAction()
|
||||||
|
object PauseRecordingVoiceMessage : MessageComposerAction()
|
||||||
|
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : MessageComposerAction()
|
||||||
|
object PlayOrPauseRecordingPlayback : MessageComposerAction()
|
||||||
|
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction()
|
||||||
}
|
}
|
@ -36,7 +36,7 @@ import im.vector.app.databinding.ComposerLayoutBinding
|
|||||||
/**
|
/**
|
||||||
* Encapsulate the timeline composer UX.
|
* Encapsulate the timeline composer UX.
|
||||||
*/
|
*/
|
||||||
class TextComposerView @JvmOverloads constructor(
|
class MessageComposerView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
@ -20,13 +20,13 @@ import androidx.annotation.StringRes
|
|||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
import im.vector.app.features.command.Command
|
import im.vector.app.features.command.Command
|
||||||
|
|
||||||
sealed class TextComposerViewEvents : VectorViewEvents {
|
sealed class MessageComposerViewEvents : VectorViewEvents {
|
||||||
|
|
||||||
data class AnimateSendButtonVisibility(val isVisible: Boolean) : TextComposerViewEvents()
|
data class AnimateSendButtonVisibility(val isVisible: Boolean) : MessageComposerViewEvents()
|
||||||
|
|
||||||
data class ShowMessage(val message: String) : TextComposerViewEvents()
|
data class ShowMessage(val message: String) : MessageComposerViewEvents()
|
||||||
|
|
||||||
abstract class SendMessageResult : TextComposerViewEvents()
|
abstract class SendMessageResult : MessageComposerViewEvents()
|
||||||
|
|
||||||
object MessageSent : SendMessageResult()
|
object MessageSent : SendMessageResult()
|
||||||
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
|
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
|
||||||
@ -36,10 +36,12 @@ sealed class TextComposerViewEvents : VectorViewEvents {
|
|||||||
data class SlashCommandResultOk(@StringRes val messageRes: Int? = null) : SendMessageResult()
|
data class SlashCommandResultOk(@StringRes val messageRes: Int? = null) : SendMessageResult()
|
||||||
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
|
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
|
||||||
|
|
||||||
data class OpenRoomMemberProfile(val userId: String) : TextComposerViewEvents()
|
data class OpenRoomMemberProfile(val userId: String) : MessageComposerViewEvents()
|
||||||
|
|
||||||
// TODO Remove
|
// TODO Remove
|
||||||
object SlashCommandNotImplemented : SendMessageResult()
|
object SlashCommandNotImplemented : SendMessageResult()
|
||||||
|
|
||||||
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : TextComposerViewEvents()
|
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : MessageComposerViewEvents()
|
||||||
|
|
||||||
|
data class VoicePlaybackOrRecordingFailure(val throwable: Throwable) : MessageComposerViewEvents()
|
||||||
}
|
}
|
@ -16,24 +16,27 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.composer
|
package im.vector.app.features.home.room.detail.composer
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.attachments.toContentAttachmentData
|
||||||
import im.vector.app.features.command.CommandParser
|
import im.vector.app.features.command.CommandParser
|
||||||
import im.vector.app.features.command.ParsedCommand
|
import im.vector.app.features.command.ParsedCommand
|
||||||
import im.vector.app.features.home.room.detail.ChatEffect
|
import im.vector.app.features.home.room.detail.ChatEffect
|
||||||
|
import im.vector.app.features.home.room.detail.RoomDetailFragment
|
||||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
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.home.room.detail.toMessageType
|
||||||
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import im.vector.app.features.voice.VoicePlayerHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
@ -55,13 +58,15 @@ import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
|||||||
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
|
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class TextComposerViewModel @AssistedInject constructor(
|
class MessageComposerViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: TextComposerViewState,
|
@Assisted initialState: MessageComposerViewState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val rainbowGenerator: RainbowGenerator
|
private val rainbowGenerator: RainbowGenerator,
|
||||||
) : VectorViewModel<TextComposerViewState, TextComposerAction, TextComposerViewEvents>(initialState) {
|
private val voiceMessageHelper: VoiceMessageHelper,
|
||||||
|
private val voicePlayerHelper: VoicePlayerHelper
|
||||||
|
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
|
||||||
|
|
||||||
private val room = session.getRoom(initialState.roomId)!!
|
private val room = session.getRoom(initialState.roomId)!!
|
||||||
|
|
||||||
@ -74,26 +79,32 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
subscribeToStateInternal()
|
subscribeToStateInternal()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: TextComposerAction) {
|
override fun handle(action: MessageComposerAction) {
|
||||||
Timber.v("Handle action: $action")
|
Timber.v("Handle action: $action")
|
||||||
when (action) {
|
when (action) {
|
||||||
is TextComposerAction.EnterEditMode -> handleEnterEditMode(action)
|
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
|
||||||
is TextComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
|
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
|
||||||
is TextComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
|
is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||||
is TextComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
|
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
|
||||||
is TextComposerAction.SaveDraft -> handleSaveDraft(action)
|
is MessageComposerAction.SaveDraft -> handleSaveDraft(action)
|
||||||
is TextComposerAction.SendMessage -> handleSendMessage(action)
|
is MessageComposerAction.SendMessage -> handleSendMessage(action)
|
||||||
is TextComposerAction.UserIsTyping -> handleUserIsTyping(action)
|
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action)
|
||||||
is TextComposerAction.OnTextChanged -> handleOnTextChanged(action)
|
is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action)
|
||||||
is TextComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
|
is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
|
||||||
|
MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
|
||||||
|
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled)
|
||||||
|
is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
|
||||||
|
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
|
||||||
|
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
|
||||||
|
is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOnVoiceRecordingUiStateChanged(action: TextComposerAction.OnVoiceRecordingUiStateChanged) = setState {
|
private fun handleOnVoiceRecordingUiStateChanged(action: MessageComposerAction.OnVoiceRecordingUiStateChanged) = setState {
|
||||||
copy(voiceRecordingUiState = action.uiState)
|
copy(voiceRecordingUiState = action.uiState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) {
|
private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
|
||||||
setState {
|
setState {
|
||||||
// Makes sure currentComposerText is upToDate when accessing further setState
|
// Makes sure currentComposerText is upToDate when accessing further setState
|
||||||
currentComposerText = action.text
|
currentComposerText = action.text
|
||||||
@ -103,7 +114,7 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun subscribeToStateInternal() {
|
private fun subscribeToStateInternal() {
|
||||||
onEach(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage, TextComposerViewState::isVoiceRecording) { _, _, _ ->
|
onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage, MessageComposerViewState::isVoiceRecording) { _, _, _ ->
|
||||||
updateIsSendButtonVisibility(false)
|
updateIsSendButtonVisibility(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,16 +122,16 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
private fun updateIsSendButtonVisibility(triggerAnimation: Boolean) = setState {
|
private fun updateIsSendButtonVisibility(triggerAnimation: Boolean) = setState {
|
||||||
val isSendButtonVisible = isComposerVisible && (sendMode !is SendMode.REGULAR || currentComposerText.isNotBlank())
|
val isSendButtonVisible = isComposerVisible && (sendMode !is SendMode.REGULAR || currentComposerText.isNotBlank())
|
||||||
if (this.isSendButtonVisible != isSendButtonVisible && triggerAnimation) {
|
if (this.isSendButtonVisible != isSendButtonVisible && triggerAnimation) {
|
||||||
_viewEvents.post(TextComposerViewEvents.AnimateSendButtonVisibility(isSendButtonVisible))
|
_viewEvents.post(MessageComposerViewEvents.AnimateSendButtonVisibility(isSendButtonVisible))
|
||||||
}
|
}
|
||||||
copy(isSendButtonVisible = isSendButtonVisible)
|
copy(isSendButtonVisible = isSendButtonVisible)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEnterRegularMode(action: TextComposerAction.EnterRegularMode) = setState {
|
private fun handleEnterRegularMode(action: MessageComposerAction.EnterRegularMode) = setState {
|
||||||
copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing))
|
copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEnterEditMode(action: TextComposerAction.EnterEditMode) {
|
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
|
||||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||||
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent())) }
|
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent())) }
|
||||||
}
|
}
|
||||||
@ -134,19 +145,19 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEnterQuoteMode(action: TextComposerAction.EnterQuoteMode) {
|
private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
|
||||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||||
setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) }
|
setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEnterReplyMode(action: TextComposerAction.EnterReplyMode) {
|
private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) {
|
||||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||||
setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) }
|
setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSendMessage(action: TextComposerAction.SendMessage) {
|
private fun handleSendMessage(action: MessageComposerAction.SendMessage) {
|
||||||
withState { state ->
|
withState { state ->
|
||||||
when (state.sendMode) {
|
when (state.sendMode) {
|
||||||
is SendMode.REGULAR -> {
|
is SendMode.REGULAR -> {
|
||||||
@ -154,22 +165,22 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
is ParsedCommand.ErrorNotACommand -> {
|
is ParsedCommand.ErrorNotACommand -> {
|
||||||
// Send the text message to the room
|
// Send the text message to the room
|
||||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.ErrorSyntax -> {
|
is ParsedCommand.ErrorSyntax -> {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandError(slashCommandResult.command))
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandError(slashCommandResult.command))
|
||||||
}
|
}
|
||||||
is ParsedCommand.ErrorEmptySlashCommand -> {
|
is ParsedCommand.ErrorEmptySlashCommand -> {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown("/"))
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown("/"))
|
||||||
}
|
}
|
||||||
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
|
||||||
}
|
}
|
||||||
is ParsedCommand.SendPlainText -> {
|
is ParsedCommand.SendPlainText -> {
|
||||||
// Send the text message to the room, without markdown
|
// Send the text message to the room, without markdown
|
||||||
room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
|
room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
|
||||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.ChangeRoomName -> {
|
is ParsedCommand.ChangeRoomName -> {
|
||||||
@ -186,11 +197,11 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
is ParsedCommand.ClearScalarToken -> {
|
is ParsedCommand.ClearScalarToken -> {
|
||||||
// TODO
|
// TODO
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented)
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandNotImplemented)
|
||||||
}
|
}
|
||||||
is ParsedCommand.SetMarkdown -> {
|
is ParsedCommand.SetMarkdown -> {
|
||||||
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
|
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk(
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(
|
||||||
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
|
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
@ -218,21 +229,21 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
is ParsedCommand.SendEmote -> {
|
is ParsedCommand.SendEmote -> {
|
||||||
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown)
|
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown)
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.SendRainbow -> {
|
is ParsedCommand.SendRainbow -> {
|
||||||
slashCommandResult.message.toString().let {
|
slashCommandResult.message.toString().let {
|
||||||
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
|
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
|
||||||
}
|
}
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.SendRainbowEmote -> {
|
is ParsedCommand.SendRainbowEmote -> {
|
||||||
slashCommandResult.message.toString().let {
|
slashCommandResult.message.toString().let {
|
||||||
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
|
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
|
||||||
}
|
}
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.SendSpoiler -> {
|
is ParsedCommand.SendSpoiler -> {
|
||||||
@ -240,22 +251,22 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
|
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
|
||||||
"<span data-mx-spoiler>${slashCommandResult.message}</span>"
|
"<span data-mx-spoiler>${slashCommandResult.message}</span>"
|
||||||
)
|
)
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.SendShrug -> {
|
is ParsedCommand.SendShrug -> {
|
||||||
sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message)
|
sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message)
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.SendLenny -> {
|
is ParsedCommand.SendLenny -> {
|
||||||
sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message)
|
sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message)
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.SendChatEffect -> {
|
is ParsedCommand.SendChatEffect -> {
|
||||||
sendChatEffect(slashCommandResult)
|
sendChatEffect(slashCommandResult)
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.ChangeTopic -> {
|
is ParsedCommand.ChangeTopic -> {
|
||||||
@ -274,25 +285,25 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
handleChangeAvatarForRoomSlashCommand(slashCommandResult)
|
handleChangeAvatarForRoomSlashCommand(slashCommandResult)
|
||||||
}
|
}
|
||||||
is ParsedCommand.ShowUser -> {
|
is ParsedCommand.ShowUser -> {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
handleWhoisSlashCommand(slashCommandResult)
|
handleWhoisSlashCommand(slashCommandResult)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.DiscardSession -> {
|
is ParsedCommand.DiscardSession -> {
|
||||||
if (room.isEncrypted()) {
|
if (room.isEncrypted()) {
|
||||||
session.cryptoService().discardOutboundSession(room.roomId)
|
session.cryptoService().discardOutboundSession(room.roomId)
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
popDraft()
|
popDraft()
|
||||||
} else {
|
} else {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
_viewEvents.post(
|
_viewEvents.post(
|
||||||
TextComposerViewEvents
|
MessageComposerViewEvents
|
||||||
.ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled))
|
.ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ParsedCommand.CreateSpace -> {
|
is ParsedCommand.CreateSpace -> {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading)
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val params = CreateSpaceParams().apply {
|
val params = CreateSpaceParams().apply {
|
||||||
@ -308,15 +319,15 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
true
|
true
|
||||||
)
|
)
|
||||||
popDraft()
|
popDraft()
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
is ParsedCommand.AddToSpace -> {
|
is ParsedCommand.AddToSpace -> {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading)
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
session.spaceService().getSpace(slashCommandResult.spaceId)
|
session.spaceService().getSpace(slashCommandResult.spaceId)
|
||||||
@ -327,22 +338,22 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
popDraft()
|
popDraft()
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
is ParsedCommand.JoinSpace -> {
|
is ParsedCommand.JoinSpace -> {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading)
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias)
|
session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias)
|
||||||
popDraft()
|
popDraft()
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
@ -352,21 +363,21 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
try {
|
try {
|
||||||
session.getRoom(slashCommandResult.roomId)?.leave(null)
|
session.getRoom(slashCommandResult.roomId)?.leave(null)
|
||||||
popDraft()
|
popDraft()
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
is ParsedCommand.UpgradeRoom -> {
|
is ParsedCommand.UpgradeRoom -> {
|
||||||
_viewEvents.post(
|
_viewEvents.post(
|
||||||
TextComposerViewEvents.ShowRoomUpgradeDialog(
|
MessageComposerViewEvents.ShowRoomUpgradeDialog(
|
||||||
slashCommandResult.newVersion,
|
slashCommandResult.newVersion,
|
||||||
room.roomSummary()?.isPublic ?: false
|
room.roomSummary()?.isPublic ?: false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
@ -391,7 +402,7 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
Timber.w("Same message content, do not send edition")
|
Timber.w("Same message content, do not send edition")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is SendMode.QUOTE -> {
|
is SendMode.QUOTE -> {
|
||||||
@ -412,13 +423,13 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
} else {
|
} else {
|
||||||
room.sendFormattedTextMessage(finalText, htmlText)
|
room.sendFormattedTextMessage(finalText, htmlText)
|
||||||
}
|
}
|
||||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is SendMode.REPLY -> {
|
is SendMode.REPLY -> {
|
||||||
state.sendMode.timelineEvent.let {
|
state.sendMode.timelineEvent.let {
|
||||||
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
||||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -467,7 +478,7 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUserIsTyping(action: TextComposerAction.UserIsTyping) {
|
private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) {
|
||||||
if (vectorPreferences.sendTypingNotifs()) {
|
if (vectorPreferences.sendTypingNotifs()) {
|
||||||
if (action.isTyping) {
|
if (action.isTyping) {
|
||||||
room.userIsTyping()
|
room.userIsTyping()
|
||||||
@ -495,13 +506,13 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
try {
|
try {
|
||||||
session.joinRoom(command.roomAlias, command.reason, emptyList())
|
session.joinRoom(command.roomAlias, command.reason, emptyList())
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
session.getRoomSummary(command.roomAlias)
|
session.getRoomSummary(command.roomAlias)
|
||||||
?.roomId
|
?.roomId
|
||||||
?.let {
|
?.let {
|
||||||
_viewEvents.post(TextComposerViewEvents.JoinRoomCommandSuccess(it))
|
_viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -648,7 +659,7 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleWhoisSlashCommand(whois: ParsedCommand.ShowUser) {
|
private fun handleWhoisSlashCommand(whois: ParsedCommand.ShowUser) {
|
||||||
_viewEvents.post(TextComposerViewEvents.OpenRoomMemberProfile(whois.userId))
|
_viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendPrefixedMessage(prefix: String, message: CharSequence) {
|
private fun sendPrefixedMessage(prefix: String, message: CharSequence) {
|
||||||
@ -665,7 +676,7 @@ class TextComposerViewModel @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 handleSaveDraft(action: TextComposerAction.SaveDraft) = withState {
|
private fun handleSaveDraft(action: MessageComposerAction.SaveDraft) = 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 -> {
|
||||||
@ -688,24 +699,88 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleStartRecordingVoiceMessage() {
|
||||||
|
try {
|
||||||
|
voiceMessageHelper.startRecording()
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) {
|
||||||
|
voiceMessageHelper.stopPlayback()
|
||||||
|
if (isCancelled) {
|
||||||
|
voiceMessageHelper.deleteRecording()
|
||||||
|
} else {
|
||||||
|
voiceMessageHelper.stopRecording()?.let { audioType ->
|
||||||
|
if (audioType.duration > 1000) {
|
||||||
|
room.sendMedia(audioType.toContentAttachmentData(), false, emptySet())
|
||||||
|
} else {
|
||||||
|
voiceMessageHelper.deleteRecording()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
// Download can fail
|
||||||
|
val audioFile = session.fileService().downloadFile(action.messageAudioContent)
|
||||||
|
// Conversion can fail, fallback to the original file in this case and let the player fail for us
|
||||||
|
val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile
|
||||||
|
// Play can fail
|
||||||
|
voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePlayOrPauseRecordingPlayback() {
|
||||||
|
voiceMessageHelper.startOrPauseRecordingPlayback()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
|
||||||
|
voiceMessageHelper.stopAllVoiceActions(deleteRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePauseRecordingVoiceMessage() {
|
||||||
|
voiceMessageHelper.pauseRecording()
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) {
|
private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) {
|
||||||
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading)
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val event = try {
|
val event = try {
|
||||||
block()
|
block()
|
||||||
popDraft()
|
popDraft()
|
||||||
TextComposerViewEvents.SlashCommandResultOk()
|
MessageComposerViewEvents.SlashCommandResultOk()
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
TextComposerViewEvents.SlashCommandResultError(failure)
|
MessageComposerViewEvents.SlashCommandResultError(failure)
|
||||||
}
|
}
|
||||||
_viewEvents.post(event)
|
_viewEvents.post(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : MavericksAssistedViewModelFactory<TextComposerViewModel, TextComposerViewState> {
|
interface Factory {
|
||||||
override fun create(initialState: TextComposerViewState): TextComposerViewModel
|
fun create(initialState: MessageComposerViewState): MessageComposerViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MavericksViewModelFactory<TextComposerViewModel, TextComposerViewState> by hiltMavericksViewModelFactory()
|
/**
|
||||||
|
* We're unable to create this ViewModel with `by hiltMavericksViewModelFactory()` due to the
|
||||||
|
* VoiceMessagePlaybackTracker being ActivityScoped
|
||||||
|
*
|
||||||
|
* This factory allows us to provide the ViewModel instance from the Fragment directly
|
||||||
|
* bypassing the Singleton scope requirement
|
||||||
|
*/
|
||||||
|
companion object : MavericksViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: MessageComposerViewState): MessageComposerViewModel {
|
||||||
|
val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.messageComposerViewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -42,7 +42,7 @@ sealed class SendMode(open val text: String) {
|
|||||||
data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
|
data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TextComposerViewState(
|
data class MessageComposerViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val canSendMessage: Boolean = true,
|
val canSendMessage: Boolean = true,
|
||||||
val isSendButtonVisible: Boolean = false,
|
val isSendButtonVisible: Boolean = false,
|
@ -103,7 +103,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||||||
voiceMessageViews.renderVisibilityChanged(parentChanged, visibility)
|
voiceMessageViews.renderVisibilityChanged(parentChanged, visibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun display(recordingState: RecordingUiState) {
|
fun render(recordingState: RecordingUiState) {
|
||||||
if (lastKnownState == recordingState) return
|
if (lastKnownState == recordingState) return
|
||||||
lastKnownState = recordingState
|
lastKnownState = recordingState
|
||||||
when (recordingState) {
|
when (recordingState) {
|
||||||
|
@ -199,7 +199,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<im.vector.app.features.home.room.detail.composer.TextComposerView
|
<im.vector.app.features.home.room.detail.composer.MessageComposerView
|
||||||
android:id="@+id/composerLayout"
|
android:id="@+id/composerLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
Loading…
Reference in New Issue
Block a user