Skip to content

Commit

Permalink
Merge pull request #7285 from vector-im/tech/split-timelinefragment
Browse files Browse the repository at this point in the history
Refactor: split TimelineFragment into MessageComposerFragment and VoiceRecorderFragment
  • Loading branch information
bmarty authored Oct 5, 2022
2 parents 80c210e + e6a2d50 commit 9335242
Show file tree
Hide file tree
Showing 14 changed files with 1,089 additions and 812 deletions.
1 change: 1 addition & 0 deletions changelog.d/7285.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Refactor TimelineFragment, split it into MessageComposerFragment and VoiceRecorderFragment.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
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.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
Expand Down Expand Up @@ -77,6 +78,8 @@ data class RoomDetailViewState(
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(),
val typingUsers: List<SenderInfo>? = null,
val isSharingLiveLocation: Boolean = false,
val showKeyboardWhenPresented: Boolean = false,
val sharedData: SharedData? = null,
) : MavericksState {

constructor(args: TimelineArgs) : this(
Expand All @@ -86,7 +89,9 @@ data class RoomDetailViewState(
// Also highlight the target event, if any
highlightedEventId = args.eventId,
switchToParentSpace = args.switchToParentSpace,
rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId
rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId,
showKeyboardWhenPresented = args.threadTimelineArgs?.showKeyboard.orFalse(),
sharedData = args.sharedData,
)

fun isCallOptionAvailable(): Boolean {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent

sealed class MessageComposerAction : VectorViewModelAction {
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
data class EnterEditMode(val eventId: String, val text: String) : MessageComposerAction()
data class EnterQuoteMode(val eventId: String, val text: String) : MessageComposerAction()
data class EnterReplyMode(val eventId: String, val text: String) : MessageComposerAction()
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : MessageComposerAction()
data class EnterEditMode(val eventId: String) : MessageComposerAction()
data class EnterQuoteMode(val eventId: String) : MessageComposerAction()
data class EnterReplyMode(val eventId: String) : MessageComposerAction()
data class EnterRegularMode(val fromSharing: Boolean) : MessageComposerAction()
data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
data class OnEntersBackground(val composerText: String) : MessageComposerAction()
data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction()
data class InsertUserDisplayName(val userId: String) : MessageComposerAction()

// Voice Message
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,4 @@ class MessageComposerView @JvmOverloads constructor(
}
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ sealed class MessageComposerViewEvents : VectorViewEvents {
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : MessageComposerViewEvents()

data class VoicePlaybackOrRecordingFailure(val throwable: Throwable) : MessageComposerViewEvents()

data class InsertUserDisplayName(val userId: String) : MessageComposerViewEvents()
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class MessageComposerViewModel @AssistedInject constructor(
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
}
}

Expand Down Expand Up @@ -144,7 +145,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}

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) {
Expand Down Expand Up @@ -181,13 +182,13 @@ class MessageComposerViewModel @AssistedInject constructor(

private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
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) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Reply(timelineEvent, action.text)) }
setState { copy(sendMode = SendMode.Reply(timelineEvent, currentComposerText)) }
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ import kotlin.random.Random
*/
sealed interface SendMode {
data class Regular(
val text: String,
val text: CharSequence,
val fromSharing: Boolean,
// This is necessary for forcing refresh on selectSubscribe
private val random: Int = Random.nextInt()
) : SendMode

data class Quote(val timelineEvent: TimelineEvent, val text: String) : SendMode
data class Edit(val timelineEvent: TimelineEvent, val text: String) : SendMode
data class Reply(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: CharSequence) : SendMode
data class Reply(val timelineEvent: TimelineEvent, val text: CharSequence) : SendMode
data class Voice(val text: String) : SendMode
}

Expand All @@ -66,7 +66,8 @@ data class MessageComposerViewState(
val rootThreadEventId: String? = null,
val startsThread: Boolean = 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 {

val isVoiceRecording = when (voiceRecordingUiState) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
override fun invoke(p1: View) {
attributes.threadCallback?.onThreadSummaryClicked(attributes.informationData.eventId, attributes.threadDetails?.isRootThread ?: false)
Expand All @@ -95,7 +89,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder>(
holder.memberNameView.isVisible = true
holder.memberNameView.text = attributes.informationData.memberName
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
holder.memberNameView.onClick(_memberNameClickListener)
holder.memberNameView.onClick(attributes.memberClickListener)
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
} else {
holder.memberNameView.setOnClickListener(null)
Expand Down
13 changes: 13 additions & 0 deletions vector/src/main/res/layout/fragment_composer.xml
Original file line number Diff line number Diff line change
@@ -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" />
Loading

0 comments on commit 9335242

Please sign in to comment.