Merge pull request #7285 from vector-im/tech/split-timelinefragment
Refactor: split TimelineFragment into MessageComposerFragment and VoiceRecorderFragment
This commit is contained in:
commit
9335242ce1
1
changelog.d/7285.misc
Normal file
1
changelog.d/7285.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Refactor TimelineFragment, split it into MessageComposerFragment and VoiceRecorderFragment.
|
@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async
|
|||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
||||||
|
import im.vector.app.features.share.SharedData
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||||
@ -77,6 +78,8 @@ data class RoomDetailViewState(
|
|||||||
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(),
|
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(),
|
||||||
val typingUsers: List<SenderInfo>? = null,
|
val typingUsers: List<SenderInfo>? = null,
|
||||||
val isSharingLiveLocation: Boolean = false,
|
val isSharingLiveLocation: Boolean = false,
|
||||||
|
val showKeyboardWhenPresented: Boolean = false,
|
||||||
|
val sharedData: SharedData? = null,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(args: TimelineArgs) : this(
|
constructor(args: TimelineArgs) : this(
|
||||||
@ -86,7 +89,9 @@ data class RoomDetailViewState(
|
|||||||
// Also highlight the target event, if any
|
// Also highlight the target event, if any
|
||||||
highlightedEventId = args.eventId,
|
highlightedEventId = args.eventId,
|
||||||
switchToParentSpace = args.switchToParentSpace,
|
switchToParentSpace = args.switchToParentSpace,
|
||||||
rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId
|
rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId,
|
||||||
|
showKeyboardWhenPresented = args.threadTimelineArgs?.showKeyboard.orFalse(),
|
||||||
|
sharedData = args.sharedData,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun isCallOptionAvailable(): Boolean {
|
fun isCallOptionAvailable(): Boolean {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -24,14 +24,15 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
|||||||
|
|
||||||
sealed class MessageComposerAction : VectorViewModelAction {
|
sealed class MessageComposerAction : VectorViewModelAction {
|
||||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
|
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
|
||||||
data class EnterEditMode(val eventId: String, val text: String) : MessageComposerAction()
|
data class EnterEditMode(val eventId: String) : MessageComposerAction()
|
||||||
data class EnterQuoteMode(val eventId: String, val text: String) : MessageComposerAction()
|
data class EnterQuoteMode(val eventId: String) : MessageComposerAction()
|
||||||
data class EnterReplyMode(val eventId: String, val text: String) : MessageComposerAction()
|
data class EnterReplyMode(val eventId: String) : MessageComposerAction()
|
||||||
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : MessageComposerAction()
|
data class EnterRegularMode(val fromSharing: Boolean) : MessageComposerAction()
|
||||||
data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
|
data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
|
||||||
data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
|
data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
|
||||||
data class OnEntersBackground(val composerText: String) : MessageComposerAction()
|
data class OnEntersBackground(val composerText: String) : MessageComposerAction()
|
||||||
data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction()
|
data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction()
|
||||||
|
data class InsertUserDisplayName(val userId: String) : MessageComposerAction()
|
||||||
|
|
||||||
// Voice Message
|
// Voice Message
|
||||||
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
|
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
|
||||||
|
@ -0,0 +1,803 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.composer
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.vanniktech.emoji.EmojiPopup
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.error.fatalError
|
||||||
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
|
import im.vector.app.core.extensions.showKeyboard
|
||||||
|
import im.vector.app.core.glide.GlideApp
|
||||||
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.core.platform.lifecycleAwareLazy
|
||||||
|
import im.vector.app.core.platform.showOptimizedSnackbar
|
||||||
|
import im.vector.app.core.resources.BuildMeta
|
||||||
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
|
import im.vector.app.core.utils.checkPermissions
|
||||||
|
import im.vector.app.core.utils.onPermissionDeniedDialog
|
||||||
|
import im.vector.app.core.utils.registerForPermissionsResult
|
||||||
|
import im.vector.app.databinding.FragmentComposerBinding
|
||||||
|
import im.vector.app.features.VectorFeatures
|
||||||
|
import im.vector.app.features.attachments.AttachmentTypeSelectorView
|
||||||
|
import im.vector.app.features.attachments.AttachmentsHelper
|
||||||
|
import im.vector.app.features.attachments.ContactAttachment
|
||||||
|
import im.vector.app.features.attachments.ShareIntentHandler
|
||||||
|
import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity
|
||||||
|
import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs
|
||||||
|
import im.vector.app.features.attachments.toGroupedContentAttachmentData
|
||||||
|
import im.vector.app.features.command.Command
|
||||||
|
import im.vector.app.features.command.ParsedCommand
|
||||||
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import im.vector.app.features.home.room.detail.AutoCompleter
|
||||||
|
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||||
|
import im.vector.app.features.home.room.detail.TimelineViewModel
|
||||||
|
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData
|
||||||
|
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
|
||||||
|
import im.vector.app.features.html.EventHtmlRenderer
|
||||||
|
import im.vector.app.features.html.PillImageSpan
|
||||||
|
import im.vector.app.features.html.PillsPostProcessor
|
||||||
|
import im.vector.app.features.location.LocationSharingMode
|
||||||
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
|
import im.vector.app.features.poll.PollMode
|
||||||
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import im.vector.app.features.share.SharedData
|
||||||
|
import im.vector.app.features.voice.VoiceFailure
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import org.commonmark.parser.Parser
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
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.MessageBeaconInfoContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
import reactivecircus.flowbinding.android.view.focusChanges
|
||||||
|
import reactivecircus.flowbinding.android.widget.textChanges
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), AttachmentsHelper.Callback, AttachmentTypeSelectorView.Callback {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ircPattern = " (IRC)"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject lateinit var autoCompleterFactory: AutoCompleter.Factory
|
||||||
|
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
@Inject lateinit var matrixItemColorProvider: MatrixItemColorProvider
|
||||||
|
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
|
||||||
|
@Inject lateinit var dimensionConverter: DimensionConverter
|
||||||
|
@Inject lateinit var imageContentRenderer: ImageContentRenderer
|
||||||
|
@Inject lateinit var shareIntentHandler: ShareIntentHandler
|
||||||
|
@Inject lateinit var pillsPostProcessorFactory: PillsPostProcessor.Factory
|
||||||
|
@Inject lateinit var vectorPreferences: VectorPreferences
|
||||||
|
@Inject lateinit var vectorFeatures: VectorFeatures
|
||||||
|
@Inject lateinit var buildMeta: BuildMeta
|
||||||
|
@Inject lateinit var session: Session
|
||||||
|
|
||||||
|
private val roomId: String get() = withState(timelineViewModel) { it.roomId }
|
||||||
|
|
||||||
|
private val autoCompleter: AutoCompleter by lazy {
|
||||||
|
autoCompleterFactory.create(roomId, isThreadTimeLine())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val pillsPostProcessor by lazy {
|
||||||
|
pillsPostProcessorFactory.create(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val emojiPopup: EmojiPopup by lifecycleAwareLazy {
|
||||||
|
createEmojiPopup()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val glideRequests by lazy {
|
||||||
|
GlideApp.with(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isEmojiKeyboardVisible: Boolean
|
||||||
|
get() = vectorPreferences.showEmojiKeyboard()
|
||||||
|
|
||||||
|
private var lockSendButton = false
|
||||||
|
|
||||||
|
private lateinit var attachmentsHelper: AttachmentsHelper
|
||||||
|
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
||||||
|
|
||||||
|
private val timelineViewModel: TimelineViewModel by activityViewModel()
|
||||||
|
private val messageComposerViewModel: MessageComposerViewModel by activityViewModel()
|
||||||
|
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentComposerBinding {
|
||||||
|
return FragmentComposerBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
||||||
|
|
||||||
|
attachmentsHelper = AttachmentsHelper(requireContext(), this, buildMeta).register()
|
||||||
|
|
||||||
|
setupComposer()
|
||||||
|
setupEmojiButton()
|
||||||
|
|
||||||
|
messageComposerViewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is MessageComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
|
||||||
|
is MessageComposerViewEvents.SlashCommandConfirmationRequest -> handleSlashCommandConfirmationRequest(it)
|
||||||
|
is MessageComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
|
||||||
|
is MessageComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
|
||||||
|
is MessageComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
|
||||||
|
is MessageComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it)
|
||||||
|
is MessageComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId)
|
||||||
|
is MessageComposerViewEvents.VoicePlaybackOrRecordingFailure -> {
|
||||||
|
if (it.throwable is VoiceFailure.UnableToRecord) {
|
||||||
|
onCannotRecord()
|
||||||
|
}
|
||||||
|
showErrorInSnackbar(it.throwable)
|
||||||
|
}
|
||||||
|
is MessageComposerViewEvents.InsertUserDisplayName -> insertUserDisplayNameInTextEditor(it.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
|
||||||
|
if (!canSend.boolean()) {
|
||||||
|
return@onEach
|
||||||
|
}
|
||||||
|
when (mode) {
|
||||||
|
is SendMode.Regular -> renderRegularMode(mode.text.toString())
|
||||||
|
is SendMode.Edit -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text.toString())
|
||||||
|
is SendMode.Quote -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.action_quote, mode.text.toString())
|
||||||
|
is SendMode.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text.toString())
|
||||||
|
is SendMode.Voice -> renderVoiceMessageMode(mode.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
handleShareData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
|
||||||
|
// we're rotating, maintain any active recordings
|
||||||
|
} else {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(views.composerLayout.text.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
|
||||||
|
autoCompleter.clear()
|
||||||
|
messageComposerViewModel.endAllVoiceActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
|
||||||
|
if (mainState.tombstoneEvent != null) return@withState
|
||||||
|
|
||||||
|
views.root.isInvisible = !messageComposerState.isComposerVisible
|
||||||
|
views.composerLayout.views.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupComposer() {
|
||||||
|
val composerEditText = views.composerLayout.views.composerEditText
|
||||||
|
composerEditText.setHint(R.string.room_message_placeholder)
|
||||||
|
|
||||||
|
autoCompleter.setup(composerEditText)
|
||||||
|
|
||||||
|
observerUserTyping()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
composerEditText.setUseIncognitoKeyboard(vectorPreferences.useIncognitoKeyboard())
|
||||||
|
}
|
||||||
|
composerEditText.setSendMessageWithEnter(vectorPreferences.sendMessageWithEnter())
|
||||||
|
|
||||||
|
composerEditText.setOnEditorActionListener { v, actionId, keyEvent ->
|
||||||
|
val imeActionId = actionId and EditorInfo.IME_MASK_ACTION
|
||||||
|
val isSendAction = EditorInfo.IME_ACTION_DONE == imeActionId || EditorInfo.IME_ACTION_SEND == imeActionId
|
||||||
|
// Add external keyboard functionality (to send messages)
|
||||||
|
val externalKeyboardPressedEnter = null != keyEvent &&
|
||||||
|
!keyEvent.isShiftPressed &&
|
||||||
|
keyEvent.keyCode == KeyEvent.KEYCODE_ENTER &&
|
||||||
|
resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS
|
||||||
|
if (isSendAction || externalKeyboardPressedEnter) {
|
||||||
|
sendTextMessage(v.text)
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
|
views.composerLayout.views.composerEmojiButton.isVisible = vectorPreferences.showEmojiKeyboard()
|
||||||
|
|
||||||
|
val showKeyboard = withState(timelineViewModel) { it.showKeyboardWhenPresented }
|
||||||
|
if (isThreadTimeLine() && showKeyboard) {
|
||||||
|
// Show keyboard when the user started a thread
|
||||||
|
views.composerLayout.views.composerEditText.showKeyboard(andRequestFocus = true)
|
||||||
|
}
|
||||||
|
views.composerLayout.callback = object : MessageComposerView.Callback {
|
||||||
|
override fun onAddAttachment() {
|
||||||
|
if (!::attachmentTypeSelector.isInitialized) {
|
||||||
|
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
|
||||||
|
attachmentTypeSelector.setAttachmentVisibility(
|
||||||
|
AttachmentTypeSelectorView.Type.LOCATION,
|
||||||
|
vectorFeatures.isLocationSharingEnabled(),
|
||||||
|
)
|
||||||
|
attachmentTypeSelector.setAttachmentVisibility(
|
||||||
|
AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine()
|
||||||
|
)
|
||||||
|
attachmentTypeSelector.setAttachmentVisibility(
|
||||||
|
AttachmentTypeSelectorView.Type.VOICE_BROADCAST,
|
||||||
|
vectorFeatures.isVoiceBroadcastEnabled(), // TODO check user permission
|
||||||
|
)
|
||||||
|
}
|
||||||
|
attachmentTypeSelector.show(views.composerLayout.views.attachmentButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onExpandOrCompactChange() {
|
||||||
|
views.composerLayout.views.composerEmojiButton.isVisible = isEmojiKeyboardVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSendMessage(text: CharSequence) {
|
||||||
|
sendTextMessage(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCloseRelatedMessage() {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||||
|
return sendUri(contentUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(text: CharSequence) {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendTextMessage(text: CharSequence) {
|
||||||
|
if (lockSendButton) {
|
||||||
|
Timber.w("Send button is locked")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (text.isNotBlank()) {
|
||||||
|
// We collapse ASAP, if not there will be a slight annoying delay
|
||||||
|
views.composerLayout.collapse(true)
|
||||||
|
lockSendButton = true
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
|
||||||
|
emojiPopup.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendUri(uri: Uri): Boolean {
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
||||||
|
val isHandled = shareIntentHandler.handleIncomingShareIntent(shareIntent, ::onContentAttachmentsReady, onPlainText = {
|
||||||
|
fatalError("Should not happen as we're generating a File based share Intent", vectorPreferences.failFast())
|
||||||
|
})
|
||||||
|
if (!isHandled) {
|
||||||
|
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
return isHandled
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderRegularMode(content: String) {
|
||||||
|
autoCompleter.exitSpecialMode()
|
||||||
|
views.composerLayout.collapse()
|
||||||
|
views.composerLayout.setTextIfDifferent(content)
|
||||||
|
views.composerLayout.views.sendButton.contentDescription = getString(R.string.action_send)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSpecialMode(
|
||||||
|
event: TimelineEvent,
|
||||||
|
@DrawableRes iconRes: Int,
|
||||||
|
@StringRes descriptionRes: Int,
|
||||||
|
defaultContent: String
|
||||||
|
) {
|
||||||
|
autoCompleter.enterSpecialMode()
|
||||||
|
// switch to expanded bar
|
||||||
|
views.composerLayout.views.composerRelatedMessageTitle.apply {
|
||||||
|
text = event.senderInfo.disambiguatedDisplayName
|
||||||
|
setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@")))
|
||||||
|
}
|
||||||
|
|
||||||
|
val messageContent: MessageContent? = event.getLastMessageContent()
|
||||||
|
val nonFormattedBody = when (messageContent) {
|
||||||
|
is MessageAudioContent -> getAudioContentBodyText(messageContent)
|
||||||
|
is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||||
|
is MessageBeaconInfoContent -> getString(R.string.live_location_description)
|
||||||
|
else -> messageContent?.body.orEmpty()
|
||||||
|
}
|
||||||
|
var formattedBody: CharSequence? = null
|
||||||
|
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
|
||||||
|
val parser = Parser.builder().build()
|
||||||
|
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
|
||||||
|
formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor)
|
||||||
|
}
|
||||||
|
views.composerLayout.views.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
|
||||||
|
|
||||||
|
// Image Event
|
||||||
|
val data = event.buildImageContentRendererData(dimensionConverter.dpToPx(66))
|
||||||
|
val isImageVisible = if (data != null) {
|
||||||
|
imageContentRenderer.render(data, ImageContentRenderer.Mode.THUMBNAIL, views.composerLayout.views.composerRelatedMessageImage)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
imageContentRenderer.clear(views.composerLayout.views.composerRelatedMessageImage)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible
|
||||||
|
|
||||||
|
views.composerLayout.setTextIfDifferent(defaultContent)
|
||||||
|
|
||||||
|
views.composerLayout.views.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||||
|
views.composerLayout.views.sendButton.contentDescription = getString(descriptionRes)
|
||||||
|
|
||||||
|
avatarRenderer.render(event.senderInfo.toMatrixItem(), views.composerLayout.views.composerRelatedMessageAvatar)
|
||||||
|
|
||||||
|
views.composerLayout.expand {
|
||||||
|
if (isAdded) {
|
||||||
|
// need to do it here also when not using quick reply
|
||||||
|
focusComposerAndShowKeyboard()
|
||||||
|
views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
focusComposerAndShowKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observerUserTyping() {
|
||||||
|
if (isThreadTimeLine()) return
|
||||||
|
views.composerLayout.views.composerEditText.textChanges()
|
||||||
|
.skipInitialValue()
|
||||||
|
.debounce(300)
|
||||||
|
.map { it.isNotEmpty() }
|
||||||
|
.onEach {
|
||||||
|
Timber.d("Typing: User is typing: $it")
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.UserIsTyping(it))
|
||||||
|
}
|
||||||
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
|
||||||
|
views.composerLayout.views.composerEditText.focusChanges()
|
||||||
|
.onEach {
|
||||||
|
timelineViewModel.handle(RoomDetailAction.ComposerFocusChange(it))
|
||||||
|
}
|
||||||
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun focusComposerAndShowKeyboard() {
|
||||||
|
if (views.composerLayout.isVisible) {
|
||||||
|
views.composerLayout.views.composerEditText.showKeyboard(andRequestFocus = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSendButtonVisibilityChanged(event: MessageComposerViewEvents.AnimateSendButtonVisibility) {
|
||||||
|
if (event.isVisible) {
|
||||||
|
views.root.views.sendButton.alpha = 0f
|
||||||
|
views.root.views.sendButton.isVisible = true
|
||||||
|
views.root.views.sendButton.animate().alpha(1f).setDuration(150).start()
|
||||||
|
} else {
|
||||||
|
views.root.views.sendButton.isInvisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderVoiceMessageMode(content: String) {
|
||||||
|
ContentAttachmentData.fromJsonString(content)?.let { audioAttachmentData ->
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.InitializeVoiceRecorder(audioAttachmentData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAudioContentBodyText(messageContent: MessageAudioContent): String {
|
||||||
|
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
|
||||||
|
return if (messageContent.voiceMessageIndicator != null) {
|
||||||
|
getString(R.string.voice_message_reply_content, formattedDuration)
|
||||||
|
} else {
|
||||||
|
getString(R.string.audio_message_reply_content, messageContent.body, formattedDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEmojiPopup(): EmojiPopup {
|
||||||
|
return EmojiPopup(
|
||||||
|
rootView = views.root,
|
||||||
|
keyboardAnimationStyle = R.style.emoji_fade_animation_style,
|
||||||
|
onEmojiPopupShownListener = {
|
||||||
|
views.composerLayout.views.composerEmojiButton.apply {
|
||||||
|
contentDescription = getString(R.string.a11y_close_emoji_picker)
|
||||||
|
setImageResource(R.drawable.ic_keyboard)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEmojiPopupDismissListener = lifecycleAwareDismissAction {
|
||||||
|
views.composerLayout.views.composerEmojiButton.apply {
|
||||||
|
contentDescription = getString(R.string.a11y_open_emoji_picker)
|
||||||
|
setImageResource(R.drawable.ic_insert_emoji)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editText = views.composerLayout.views.composerEditText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure dismiss actions only trigger when the fragment is in the started state.
|
||||||
|
* EmojiPopup by default dismisses onViewDetachedFromWindow, this can cause race conditions with onDestroyView.
|
||||||
|
*/
|
||||||
|
private fun lifecycleAwareDismissAction(action: () -> Unit): () -> Unit {
|
||||||
|
return {
|
||||||
|
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupEmojiButton() {
|
||||||
|
views.composerLayout.views.composerEmojiButton.debouncedClicks {
|
||||||
|
emojiPopup.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCannotRecord() {
|
||||||
|
// Update the UI, cancel the animation
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(VoiceMessageRecorderView.RecordingUiState.Idle))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleJoinedToAnotherRoom(action: MessageComposerViewEvents.JoinRoomCommandSuccess) {
|
||||||
|
views.composerLayout.setTextIfDifferent("")
|
||||||
|
lockSendButton = false
|
||||||
|
navigator.openRoom(vectorBaseActivity, action.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSlashCommandConfirmationRequest(action: MessageComposerViewEvents.SlashCommandConfirmationRequest) {
|
||||||
|
when (action.parsedCommand) {
|
||||||
|
is ParsedCommand.UnignoreUser -> promptUnignoreUser(action.parsedCommand)
|
||||||
|
else -> TODO("Add case for ${action.parsedCommand.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
lockSendButton = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun promptUnignoreUser(command: ParsedCommand.UnignoreUser) {
|
||||||
|
MaterialAlertDialogBuilder(requireActivity())
|
||||||
|
.setTitle(R.string.room_participants_action_unignore_title)
|
||||||
|
.setMessage(getString(R.string.settings_unignore_user, command.userId))
|
||||||
|
.setPositiveButton(R.string.unignore) { _, _ ->
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.SlashCommandConfirmed(command))
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.action_cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSendMessageResult(sendMessageResult: MessageComposerViewEvents.SendMessageResult) {
|
||||||
|
when (sendMessageResult) {
|
||||||
|
is MessageComposerViewEvents.SlashCommandLoading -> {
|
||||||
|
showLoading(null)
|
||||||
|
}
|
||||||
|
is MessageComposerViewEvents.SlashCommandError -> {
|
||||||
|
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
||||||
|
}
|
||||||
|
is MessageComposerViewEvents.SlashCommandUnknown -> {
|
||||||
|
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
|
||||||
|
}
|
||||||
|
is MessageComposerViewEvents.SlashCommandResultOk -> {
|
||||||
|
handleSlashCommandResultOk(sendMessageResult.parsedCommand)
|
||||||
|
}
|
||||||
|
is MessageComposerViewEvents.SlashCommandResultError -> {
|
||||||
|
dismissLoadingDialog()
|
||||||
|
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
|
||||||
|
}
|
||||||
|
is MessageComposerViewEvents.SlashCommandNotImplemented -> {
|
||||||
|
displayCommandError(getString(R.string.not_implemented))
|
||||||
|
}
|
||||||
|
is MessageComposerViewEvents.SlashCommandNotSupportedInThreads -> {
|
||||||
|
displayCommandError(getString(R.string.command_not_supported_in_threads, sendMessageResult.command.command))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lockSendButton = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSlashCommandResultOk(parsedCommand: ParsedCommand) {
|
||||||
|
dismissLoadingDialog()
|
||||||
|
views.composerLayout.setTextIfDifferent("")
|
||||||
|
when (parsedCommand) {
|
||||||
|
is ParsedCommand.DevTools -> {
|
||||||
|
navigator.openDevTools(requireContext(), roomId)
|
||||||
|
}
|
||||||
|
is ParsedCommand.SetMarkdown -> {
|
||||||
|
showSnackWithMessage(getString(if (parsedCommand.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayCommandError(message: String) {
|
||||||
|
MaterialAlertDialogBuilder(requireActivity())
|
||||||
|
.setTitle(R.string.command_error)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnackWithMessage(message: String) {
|
||||||
|
view?.showOptimizedSnackbar(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: MessageComposerViewEvents.ShowRoomUpgradeDialog) {
|
||||||
|
val tag = MigrateRoomBottomSheet::javaClass.name
|
||||||
|
val roomId = withState(timelineViewModel) { it.roomId }
|
||||||
|
MigrateRoomBottomSheet.newInstance(roomId, roomDetailViewEvents.newVersion)
|
||||||
|
.show(parentFragmentManager, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openRoomMemberProfile(userId: String) {
|
||||||
|
navigator.openRoomMemberProfile(userId = userId, roomId = roomId, context = requireActivity())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val contentAttachmentActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
|
val data = activityResult.data ?: return@registerStartForActivityResult
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
val sendData = AttachmentsPreviewActivity.getOutput(data)
|
||||||
|
val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
|
||||||
|
timelineViewModel.handle(RoomDetailAction.SendMedia(sendData, !keepOriginalSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachmentsHelper.Callback
|
||||||
|
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
||||||
|
val grouped = attachments.toGroupedContentAttachmentData()
|
||||||
|
if (grouped.notPreviewables.isNotEmpty()) {
|
||||||
|
// Send the not previewable attachments right now (?)
|
||||||
|
timelineViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false))
|
||||||
|
}
|
||||||
|
if (grouped.previewables.isNotEmpty()) {
|
||||||
|
val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables))
|
||||||
|
contentAttachmentActivityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||||
|
val formattedContact = contactAttachment.toHumanReadable()
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.SendMessage(formattedContact, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachmentError(throwable: Throwable) {
|
||||||
|
showFailure(throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachmentTypeSelectorView.Callback
|
||||||
|
private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
|
||||||
|
if (allGranted) {
|
||||||
|
val pendingType = attachmentsHelper.pendingType
|
||||||
|
if (pendingType != null) {
|
||||||
|
attachmentsHelper.pendingType = null
|
||||||
|
launchAttachmentProcess(pendingType)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (deniedPermanently) {
|
||||||
|
activity?.onPermissionDeniedDialog(R.string.denied_permission_generic)
|
||||||
|
}
|
||||||
|
cleanUpAfterPermissionNotGranted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
||||||
|
when (type) {
|
||||||
|
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(
|
||||||
|
activity = requireActivity(),
|
||||||
|
vectorPreferences = vectorPreferences,
|
||||||
|
cameraActivityResultLauncher = attachmentCameraActivityResultLauncher,
|
||||||
|
cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher
|
||||||
|
)
|
||||||
|
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
|
||||||
|
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
|
||||||
|
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
|
||||||
|
AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
|
||||||
|
AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE)
|
||||||
|
AttachmentTypeSelectorView.Type.LOCATION -> {
|
||||||
|
navigator
|
||||||
|
.openLocationSharing(
|
||||||
|
context = requireContext(),
|
||||||
|
roomId = roomId,
|
||||||
|
mode = LocationSharingMode.STATIC_SHARING,
|
||||||
|
initialLocationData = null,
|
||||||
|
locationOwnerId = session.myUserId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(RoomDetailAction.StartVoiceBroadcast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
|
||||||
|
if (checkPermissions(type.permissions, requireActivity(), typeSelectedActivityResultLauncher)) {
|
||||||
|
launchAttachmentProcess(type)
|
||||||
|
} else {
|
||||||
|
attachmentsHelper.pendingType = type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
attachmentsHelper.onFileResult(it.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val attachmentContactActivityResultLauncher = registerStartForActivityResult {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
attachmentsHelper.onContactResult(it.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val attachmentMediaActivityResultLauncher = registerStartForActivityResult {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
attachmentsHelper.onMediaResult(it.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val attachmentCameraActivityResultLauncher = registerStartForActivityResult {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
attachmentsHelper.onCameraResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val attachmentCameraVideoActivityResultLauncher = registerStartForActivityResult {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
attachmentsHelper.onCameraVideoResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanUpAfterPermissionNotGranted() {
|
||||||
|
// Reset all pending data
|
||||||
|
timelineViewModel.pendingAction = null
|
||||||
|
attachmentsHelper.pendingType = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleShareData() {
|
||||||
|
when (val sharedData = withState(timelineViewModel) { it.sharedData }) {
|
||||||
|
is SharedData.Text -> {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(sharedData.text))
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(fromSharing = true))
|
||||||
|
}
|
||||||
|
is SharedData.Attachments -> {
|
||||||
|
// open share edition
|
||||||
|
onContentAttachmentsReady(sharedData.attachmentData)
|
||||||
|
}
|
||||||
|
null -> Timber.v("No share data to process")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
private fun insertUserDisplayNameInTextEditor(userId: String) {
|
||||||
|
val startToCompose = views.composerLayout.text.isNullOrBlank()
|
||||||
|
|
||||||
|
if (startToCompose &&
|
||||||
|
userId == session.myUserId) {
|
||||||
|
// Empty composer, current user: start an emote
|
||||||
|
views.composerLayout.views.composerEditText.setText("${Command.EMOTE.command} ")
|
||||||
|
views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.command.length + 1)
|
||||||
|
} else {
|
||||||
|
val roomMember = timelineViewModel.getMember(userId)
|
||||||
|
val displayName = sanitizeDisplayName(roomMember?.displayName ?: userId)
|
||||||
|
val pill = buildSpannedString {
|
||||||
|
append(displayName)
|
||||||
|
setSpan(
|
||||||
|
PillImageSpan(
|
||||||
|
glideRequests,
|
||||||
|
avatarRenderer,
|
||||||
|
requireContext(),
|
||||||
|
MatrixItem.UserItem(userId, displayName, roomMember?.avatarUrl)
|
||||||
|
)
|
||||||
|
.also { it.bind(views.composerLayout.views.composerEditText) },
|
||||||
|
0,
|
||||||
|
displayName.length,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
append(if (startToCompose) ": " else " ")
|
||||||
|
}
|
||||||
|
if (startToCompose) {
|
||||||
|
if (displayName.startsWith("/")) {
|
||||||
|
// Ensure displayName will not be interpreted as a Slash command
|
||||||
|
views.composerLayout.views.composerEditText.append("\\")
|
||||||
|
}
|
||||||
|
views.composerLayout.views.composerEditText.append(pill)
|
||||||
|
} else {
|
||||||
|
views.composerLayout.views.composerEditText.text?.insert(views.composerLayout.views.composerEditText.selectionStart, pill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
focusComposerAndShowKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize the display name.
|
||||||
|
*
|
||||||
|
* @param displayName the display name to sanitize
|
||||||
|
* @return the sanitized display name
|
||||||
|
*/
|
||||||
|
private fun sanitizeDisplayName(displayName: String): String {
|
||||||
|
if (displayName.endsWith(ircPattern)) {
|
||||||
|
return displayName.substring(0, displayName.length - ircPattern.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the root thread event if we are in a thread room, otherwise returns null.
|
||||||
|
*/
|
||||||
|
fun getRootThreadEventId(): String? = withState(timelineViewModel) { it.rootThreadEventId }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the current room is a Thread room, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isThreadTimeLine(): Boolean = withState(timelineViewModel) { it.isThreadTimeline() }
|
||||||
|
|
||||||
|
/** Set whether the keyboard should disable personalized learning. */
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun EditText.setUseIncognitoKeyboard(useIncognitoKeyboard: Boolean) {
|
||||||
|
imeOptions = if (useIncognitoKeyboard) {
|
||||||
|
imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||||
|
} else {
|
||||||
|
imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set whether enter should send the message or add a new line. */
|
||||||
|
private fun EditText.setSendMessageWithEnter(sendMessageWithEnter: Boolean) {
|
||||||
|
if (sendMessageWithEnter) {
|
||||||
|
inputType = inputType and EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE.inv()
|
||||||
|
imeOptions = imeOptions or EditorInfo.IME_ACTION_SEND
|
||||||
|
} else {
|
||||||
|
inputType = inputType or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
|
||||||
|
imeOptions = imeOptions and EditorInfo.IME_ACTION_SEND.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -149,12 +149,4 @@ class MessageComposerView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRoomEncrypted(isEncrypted: Boolean) {
|
|
||||||
if (isEncrypted) {
|
|
||||||
views.composerEditText.setHint(R.string.room_message_placeholder)
|
|
||||||
} else {
|
|
||||||
views.composerEditText.setHint(R.string.room_message_placeholder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -47,4 +47,6 @@ sealed class MessageComposerViewEvents : VectorViewEvents {
|
|||||||
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : MessageComposerViewEvents()
|
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : MessageComposerViewEvents()
|
||||||
|
|
||||||
data class VoicePlaybackOrRecordingFailure(val throwable: Throwable) : MessageComposerViewEvents()
|
data class VoicePlaybackOrRecordingFailure(val throwable: Throwable) : MessageComposerViewEvents()
|
||||||
|
|
||||||
|
data class InsertUserDisplayName(val userId: String) : MessageComposerViewEvents()
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
|
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
|
||||||
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
|
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
|
||||||
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
|
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
|
||||||
|
is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +145,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEnterRegularMode(action: MessageComposerAction.EnterRegularMode) = setState {
|
private fun handleEnterRegularMode(action: MessageComposerAction.EnterRegularMode) = setState {
|
||||||
copy(sendMode = SendMode.Regular(action.text, action.fromSharing))
|
copy(sendMode = SendMode.Regular(currentComposerText, action.fromSharing))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
|
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
|
||||||
@ -181,13 +182,13 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun handleEnterQuoteMode(action: MessageComposerAction.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, currentComposerText)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEnterReplyMode(action: MessageComposerAction.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, currentComposerText)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -875,7 +876,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleEnterRegularMode(MessageComposerAction.EnterRegularMode(text = "", fromSharing = false))
|
handleEnterRegularMode(MessageComposerAction.EnterRegularMode(fromSharing = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) {
|
private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) {
|
||||||
@ -943,6 +944,10 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleInsertUserDisplayName(action: MessageComposerAction.InsertUserDisplayName) {
|
||||||
|
_viewEvents.post(MessageComposerViewEvents.InsertUserDisplayName(action.userId))
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) {
|
private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) {
|
||||||
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -33,15 +33,15 @@ import kotlin.random.Random
|
|||||||
*/
|
*/
|
||||||
sealed interface SendMode {
|
sealed interface SendMode {
|
||||||
data class Regular(
|
data class Regular(
|
||||||
val text: String,
|
val text: CharSequence,
|
||||||
val fromSharing: Boolean,
|
val fromSharing: Boolean,
|
||||||
// This is necessary for forcing refresh on selectSubscribe
|
// This is necessary for forcing refresh on selectSubscribe
|
||||||
private val random: Int = Random.nextInt()
|
private val random: Int = Random.nextInt()
|
||||||
) : SendMode
|
) : SendMode
|
||||||
|
|
||||||
data class Quote(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
data class Quote(val timelineEvent: TimelineEvent, val text: CharSequence) : SendMode
|
||||||
data class Edit(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
data class Edit(val timelineEvent: TimelineEvent, val text: CharSequence) : SendMode
|
||||||
data class Reply(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
data class Reply(val timelineEvent: TimelineEvent, val text: CharSequence) : SendMode
|
||||||
data class Voice(val text: String) : SendMode
|
data class Voice(val text: String) : SendMode
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +66,8 @@ data class MessageComposerViewState(
|
|||||||
val rootThreadEventId: String? = null,
|
val rootThreadEventId: String? = null,
|
||||||
val startsThread: Boolean = false,
|
val startsThread: Boolean = false,
|
||||||
val sendMode: SendMode = SendMode.Regular("", false),
|
val sendMode: SendMode = SendMode.Regular("", false),
|
||||||
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle
|
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle,
|
||||||
|
val text: CharSequence? = null,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
val isVoiceRecording = when (voiceRecordingUiState) {
|
val isVoiceRecording = when (voiceRecordingUiState) {
|
||||||
|
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.composer.voice
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.hardware.vibrate
|
||||||
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.core.time.Clock
|
||||||
|
import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_MESSAGE
|
||||||
|
import im.vector.app.core.utils.checkPermissions
|
||||||
|
import im.vector.app.core.utils.onPermissionDeniedSnackbar
|
||||||
|
import im.vector.app.core.utils.registerForPermissionsResult
|
||||||
|
import im.vector.app.databinding.FragmentVoiceRecorderBinding
|
||||||
|
import im.vector.app.features.home.room.detail.TimelineViewModel
|
||||||
|
import im.vector.app.features.home.room.detail.composer.MessageComposerAction
|
||||||
|
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.timeline.helper.AudioMessagePlaybackTracker
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class VoiceRecorderFragment : VectorBaseFragment<FragmentVoiceRecorderBinding>() {
|
||||||
|
|
||||||
|
@Inject lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
|
||||||
|
@Inject lateinit var clock: Clock
|
||||||
|
|
||||||
|
private val timelineViewModel: TimelineViewModel by activityViewModel()
|
||||||
|
private val messageComposerViewModel: MessageComposerViewModel by activityViewModel()
|
||||||
|
|
||||||
|
private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
|
||||||
|
if (allGranted) {
|
||||||
|
// In this case, let the user start again the gesture
|
||||||
|
} else if (deniedPermanently) {
|
||||||
|
vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentVoiceRecorderBinding {
|
||||||
|
return FragmentVoiceRecorderBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
messageComposerViewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is MessageComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it.isVisible)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Removed listeners should be set again
|
||||||
|
setupVoiceMessageView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
audioMessagePlaybackTracker.pauseAllPlaybacks()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
|
||||||
|
if (mainState.tombstoneEvent != null) return@withState
|
||||||
|
|
||||||
|
val hasVoiceDraft = messageComposerState.voiceRecordingUiState is VoiceMessageRecorderView.RecordingUiState.Draft
|
||||||
|
with(views.root) {
|
||||||
|
isVisible = messageComposerState.isVoiceMessageRecorderVisible || hasVoiceDraft
|
||||||
|
render(messageComposerState.voiceRecordingUiState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSendButtonVisibilityChanged(isSendButtonVisible: Boolean) {
|
||||||
|
if (isSendButtonVisible) {
|
||||||
|
views.root.isVisible = false
|
||||||
|
} else {
|
||||||
|
views.root.alpha = 0f
|
||||||
|
views.root.isVisible = true
|
||||||
|
views.root.animate().alpha(1f).setDuration(150).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupVoiceMessageView() {
|
||||||
|
audioMessagePlaybackTracker.track(AudioMessagePlaybackTracker.RECORDING_ID, views.voiceMessageRecorderView)
|
||||||
|
views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback {
|
||||||
|
|
||||||
|
override fun onVoiceRecordingStarted() {
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
|
||||||
|
vibrate(requireContext())
|
||||||
|
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Recording(clock.epochMillis()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVoicePlaybackButtonClicked() {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseRecordingPlayback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVoiceRecordingCancelled() {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId()))
|
||||||
|
vibrate(requireContext())
|
||||||
|
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVoiceRecordingLocked() {
|
||||||
|
val startedState = withState(messageComposerViewModel) { it.voiceRecordingUiState as? VoiceMessageRecorderView.RecordingUiState.Recording }
|
||||||
|
val startTime = startedState?.recordingStartTimestamp ?: clock.epochMillis()
|
||||||
|
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Locked(startTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVoiceRecordingEnded() {
|
||||||
|
onSendVoiceMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSendVoiceMessage() {
|
||||||
|
messageComposerViewModel.handle(
|
||||||
|
MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false, rootThreadEventId = getRootThreadEventId())
|
||||||
|
)
|
||||||
|
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteVoiceMessage() {
|
||||||
|
messageComposerViewModel.handle(
|
||||||
|
MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId())
|
||||||
|
)
|
||||||
|
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecordingLimitReached() = pauseRecording()
|
||||||
|
|
||||||
|
override fun onRecordingWaveformClicked() = pauseRecording()
|
||||||
|
|
||||||
|
override fun onVoiceWaveformTouchedUp(percentage: Float, duration: Int) {
|
||||||
|
messageComposerViewModel.handle(
|
||||||
|
MessageComposerAction.VoiceWaveformTouchedUp(AudioMessagePlaybackTracker.RECORDING_ID, duration, percentage)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVoiceWaveformMoved(percentage: Float, duration: Int) {
|
||||||
|
messageComposerViewModel.handle(
|
||||||
|
MessageComposerAction.VoiceWaveformTouchedUp(AudioMessagePlaybackTracker.RECORDING_ID, duration, percentage)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRecordingUiState(state: VoiceMessageRecorderView.RecordingUiState) {
|
||||||
|
messageComposerViewModel.handle(
|
||||||
|
MessageComposerAction.OnVoiceRecordingUiStateChanged(state)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pauseRecording() {
|
||||||
|
messageComposerViewModel.handle(
|
||||||
|
MessageComposerAction.PauseRecordingVoiceMessage
|
||||||
|
)
|
||||||
|
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Draft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the root thread event if we are in a thread room, otherwise returns null.
|
||||||
|
*/
|
||||||
|
fun getRootThreadEventId(): String? = withState(timelineViewModel) { it.rootThreadEventId }
|
||||||
|
}
|
@ -63,12 +63,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _memberNameClickListener = object : ClickListener {
|
|
||||||
override fun invoke(p1: View) {
|
|
||||||
attributes.avatarCallback?.onMemberNameClicked(attributes.informationData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _threadClickListener = object : ClickListener {
|
private val _threadClickListener = object : ClickListener {
|
||||||
override fun invoke(p1: View) {
|
override fun invoke(p1: View) {
|
||||||
attributes.threadCallback?.onThreadSummaryClicked(attributes.informationData.eventId, attributes.threadDetails?.isRootThread ?: false)
|
attributes.threadCallback?.onThreadSummaryClicked(attributes.informationData.eventId, attributes.threadDetails?.isRootThread ?: false)
|
||||||
@ -95,7 +89,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder>(
|
|||||||
holder.memberNameView.isVisible = true
|
holder.memberNameView.isVisible = true
|
||||||
holder.memberNameView.text = attributes.informationData.memberName
|
holder.memberNameView.text = attributes.informationData.memberName
|
||||||
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
|
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
|
||||||
holder.memberNameView.onClick(_memberNameClickListener)
|
holder.memberNameView.onClick(attributes.memberClickListener)
|
||||||
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
} else {
|
} else {
|
||||||
holder.memberNameView.setOnClickListener(null)
|
holder.memberNameView.setOnClickListener(null)
|
||||||
|
13
vector/src/main/res/layout/fragment_composer.xml
Normal file
13
vector/src/main/res/layout/fragment_composer.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<im.vector.app.features.home.room.detail.composer.MessageComposerView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/composerLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:minHeight="56dp"
|
||||||
|
android:transitionName="composer"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
@ -119,33 +119,26 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inflatedId="@+id/failedMessagesWarningStub"
|
android:inflatedId="@+id/failedMessagesWarningStub"
|
||||||
android:layout="@layout/view_stub_failed_message_warning_layout"
|
android:layout="@layout/view_stub_failed_message_warning_layout"
|
||||||
app:layout_constraintBottom_toTopOf="@id/composerLayout"
|
app:layout_constraintBottom_toTopOf="@id/composerContainer"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:layout_height="300dp" />
|
tools:layout_height="300dp" />
|
||||||
|
|
||||||
<im.vector.app.features.home.room.detail.composer.MessageComposerView
|
<FrameLayout
|
||||||
android:id="@+id/composerLayout"
|
android:id="@+id/composerContainer"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?android:colorBackground"
|
|
||||||
android:minHeight="56dp"
|
|
||||||
android:transitionName="composer"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
|
||||||
android:id="@+id/voiceMessageRecorderView"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:visibility="visible" />
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/voiceMessageRecorderContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
<ViewStub
|
<ViewStub
|
||||||
android:id="@+id/inviteViewStub"
|
android:id="@+id/inviteViewStub"
|
||||||
@ -163,7 +156,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:barrierDirection="top"
|
app:barrierDirection="top"
|
||||||
app:constraint_referenced_ids="composerLayout,notificationAreaView,failedMessagesWarningStub" />
|
app:constraint_referenced_ids="composerContainer,notificationAreaView,failedMessagesWarningStub" />
|
||||||
|
|
||||||
<im.vector.app.core.platform.BadgeFloatingActionButton
|
<im.vector.app.core.platform.BadgeFloatingActionButton
|
||||||
android:id="@+id/jumpToBottomView"
|
android:id="@+id/jumpToBottomView"
|
||||||
|
9
vector/src/main/res/layout/fragment_voice_recorder.xml
Normal file
9
vector/src/main/res/layout/fragment_voice_recorder.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/voiceMessageRecorderView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
Loading…
x
Reference in New Issue
Block a user