Skip to content

Commit 9335242

Browse files
authored
Merge pull request #7285 from vector-im/tech/split-timelinefragment
Refactor: split TimelineFragment into MessageComposerFragment and VoiceRecorderFragment
2 parents 80c210e + e6a2d50 commit 9335242

14 files changed

+1089
-812
lines changed

changelog.d/7285.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Refactor TimelineFragment, split it into MessageComposerFragment and VoiceRecorderFragment.

vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async
2020
import com.airbnb.mvrx.MavericksState
2121
import com.airbnb.mvrx.Uninitialized
2222
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
23+
import im.vector.app.features.share.SharedData
2324
import org.matrix.android.sdk.api.extensions.orFalse
2425
import org.matrix.android.sdk.api.session.events.model.Event
2526
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
@@ -77,6 +78,8 @@ data class RoomDetailViewState(
7778
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(),
7879
val typingUsers: List<SenderInfo>? = null,
7980
val isSharingLiveLocation: Boolean = false,
81+
val showKeyboardWhenPresented: Boolean = false,
82+
val sharedData: SharedData? = null,
8083
) : MavericksState {
8184

8285
constructor(args: TimelineArgs) : this(
@@ -86,7 +89,9 @@ data class RoomDetailViewState(
8689
// Also highlight the target event, if any
8790
highlightedEventId = args.eventId,
8891
switchToParentSpace = args.switchToParentSpace,
89-
rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId
92+
rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId,
93+
showKeyboardWhenPresented = args.threadTimelineArgs?.showKeyboard.orFalse(),
94+
sharedData = args.sharedData,
9095
)
9196

9297
fun isCallOptionAvailable(): Boolean {

vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt

Lines changed: 34 additions & 765 deletions
Large diffs are not rendered by default.

vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
2424

2525
sealed class MessageComposerAction : VectorViewModelAction {
2626
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
27-
data class EnterEditMode(val eventId: String, val text: String) : MessageComposerAction()
28-
data class EnterQuoteMode(val eventId: String, val text: String) : MessageComposerAction()
29-
data class EnterReplyMode(val eventId: String, val text: String) : MessageComposerAction()
30-
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : MessageComposerAction()
27+
data class EnterEditMode(val eventId: String) : MessageComposerAction()
28+
data class EnterQuoteMode(val eventId: String) : MessageComposerAction()
29+
data class EnterReplyMode(val eventId: String) : MessageComposerAction()
30+
data class EnterRegularMode(val fromSharing: Boolean) : MessageComposerAction()
3131
data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
3232
data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
3333
data class OnEntersBackground(val composerText: String) : MessageComposerAction()
3434
data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction()
35+
data class InsertUserDisplayName(val userId: String) : MessageComposerAction()
3536

3637
// Voice Message
3738
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()

vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt

Lines changed: 803 additions & 0 deletions
Large diffs are not rendered by default.

vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,4 @@ class MessageComposerView @JvmOverloads constructor(
149149
}
150150
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
151151
}
152-
153-
fun setRoomEncrypted(isEncrypted: Boolean) {
154-
if (isEncrypted) {
155-
views.composerEditText.setHint(R.string.room_message_placeholder)
156-
} else {
157-
views.composerEditText.setHint(R.string.room_message_placeholder)
158-
}
159-
}
160152
}

vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,6 @@ sealed class MessageComposerViewEvents : VectorViewEvents {
4747
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : MessageComposerViewEvents()
4848

4949
data class VoicePlaybackOrRecordingFailure(val throwable: Throwable) : MessageComposerViewEvents()
50+
51+
data class InsertUserDisplayName(val userId: String) : MessageComposerViewEvents()
5052
}

vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class MessageComposerViewModel @AssistedInject constructor(
113113
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
114114
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
115115
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
116+
is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
116117
}
117118
}
118119

@@ -144,7 +145,7 @@ class MessageComposerViewModel @AssistedInject constructor(
144145
}
145146

146147
private fun handleEnterRegularMode(action: MessageComposerAction.EnterRegularMode) = setState {
147-
copy(sendMode = SendMode.Regular(action.text, action.fromSharing))
148+
copy(sendMode = SendMode.Regular(currentComposerText, action.fromSharing))
148149
}
149150

150151
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
@@ -181,13 +182,13 @@ class MessageComposerViewModel @AssistedInject constructor(
181182

182183
private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
183184
room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
184-
setState { copy(sendMode = SendMode.Quote(timelineEvent, action.text)) }
185+
setState { copy(sendMode = SendMode.Quote(timelineEvent, currentComposerText)) }
185186
}
186187
}
187188

188189
private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) {
189190
room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
190-
setState { copy(sendMode = SendMode.Reply(timelineEvent, action.text)) }
191+
setState { copy(sendMode = SendMode.Reply(timelineEvent, currentComposerText)) }
191192
}
192193
}
193194

@@ -875,7 +876,7 @@ class MessageComposerViewModel @AssistedInject constructor(
875876
}
876877
}
877878
}
878-
handleEnterRegularMode(MessageComposerAction.EnterRegularMode(text = "", fromSharing = false))
879+
handleEnterRegularMode(MessageComposerAction.EnterRegularMode(fromSharing = false))
879880
}
880881

881882
private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) {
@@ -943,6 +944,10 @@ class MessageComposerViewModel @AssistedInject constructor(
943944
}
944945
}
945946

947+
private fun handleInsertUserDisplayName(action: MessageComposerAction.InsertUserDisplayName) {
948+
_viewEvents.post(MessageComposerViewEvents.InsertUserDisplayName(action.userId))
949+
}
950+
946951
private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) {
947952
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
948953
viewModelScope.launch {

vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ import kotlin.random.Random
3333
*/
3434
sealed interface SendMode {
3535
data class Regular(
36-
val text: String,
36+
val text: CharSequence,
3737
val fromSharing: Boolean,
3838
// This is necessary for forcing refresh on selectSubscribe
3939
private val random: Int = Random.nextInt()
4040
) : SendMode
4141

42-
data class Quote(val timelineEvent: TimelineEvent, val text: String) : SendMode
43-
data class Edit(val timelineEvent: TimelineEvent, val text: String) : SendMode
44-
data class Reply(val timelineEvent: TimelineEvent, val text: String) : SendMode
42+
data class Quote(val timelineEvent: TimelineEvent, val text: CharSequence) : SendMode
43+
data class Edit(val timelineEvent: TimelineEvent, val text: CharSequence) : SendMode
44+
data class Reply(val timelineEvent: TimelineEvent, val text: CharSequence) : SendMode
4545
data class Voice(val text: String) : SendMode
4646
}
4747

@@ -66,7 +66,8 @@ data class MessageComposerViewState(
6666
val rootThreadEventId: String? = null,
6767
val startsThread: Boolean = false,
6868
val sendMode: SendMode = SendMode.Regular("", false),
69-
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle
69+
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle,
70+
val text: CharSequence? = null,
7071
) : MavericksState {
7172

7273
val isVoiceRecording = when (voiceRecordingUiState) {
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.home.room.detail.composer.voice
18+
19+
import android.os.Bundle
20+
import android.view.LayoutInflater
21+
import android.view.View
22+
import android.view.ViewGroup
23+
import androidx.core.view.isVisible
24+
import com.airbnb.mvrx.activityViewModel
25+
import com.airbnb.mvrx.withState
26+
import dagger.hilt.android.AndroidEntryPoint
27+
import im.vector.app.R
28+
import im.vector.app.core.hardware.vibrate
29+
import im.vector.app.core.platform.VectorBaseFragment
30+
import im.vector.app.core.time.Clock
31+
import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_MESSAGE
32+
import im.vector.app.core.utils.checkPermissions
33+
import im.vector.app.core.utils.onPermissionDeniedSnackbar
34+
import im.vector.app.core.utils.registerForPermissionsResult
35+
import im.vector.app.databinding.FragmentVoiceRecorderBinding
36+
import im.vector.app.features.home.room.detail.TimelineViewModel
37+
import im.vector.app.features.home.room.detail.composer.MessageComposerAction
38+
import im.vector.app.features.home.room.detail.composer.MessageComposerViewEvents
39+
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
40+
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
41+
import javax.inject.Inject
42+
43+
@AndroidEntryPoint
44+
class VoiceRecorderFragment : VectorBaseFragment<FragmentVoiceRecorderBinding>() {
45+
46+
@Inject lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
47+
@Inject lateinit var clock: Clock
48+
49+
private val timelineViewModel: TimelineViewModel by activityViewModel()
50+
private val messageComposerViewModel: MessageComposerViewModel by activityViewModel()
51+
52+
private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
53+
if (allGranted) {
54+
// In this case, let the user start again the gesture
55+
} else if (deniedPermanently) {
56+
vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message)
57+
}
58+
}
59+
60+
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentVoiceRecorderBinding {
61+
return FragmentVoiceRecorderBinding.inflate(inflater, container, false)
62+
}
63+
64+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
65+
super.onViewCreated(view, savedInstanceState)
66+
67+
messageComposerViewModel.observeViewEvents {
68+
when (it) {
69+
is MessageComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it.isVisible)
70+
else -> Unit
71+
}
72+
}
73+
}
74+
75+
override fun onResume() {
76+
super.onResume()
77+
78+
// Removed listeners should be set again
79+
setupVoiceMessageView()
80+
}
81+
82+
override fun onPause() {
83+
super.onPause()
84+
85+
audioMessagePlaybackTracker.pauseAllPlaybacks()
86+
}
87+
88+
override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
89+
if (mainState.tombstoneEvent != null) return@withState
90+
91+
val hasVoiceDraft = messageComposerState.voiceRecordingUiState is VoiceMessageRecorderView.RecordingUiState.Draft
92+
with(views.root) {
93+
isVisible = messageComposerState.isVoiceMessageRecorderVisible || hasVoiceDraft
94+
render(messageComposerState.voiceRecordingUiState)
95+
}
96+
}
97+
98+
private fun handleSendButtonVisibilityChanged(isSendButtonVisible: Boolean) {
99+
if (isSendButtonVisible) {
100+
views.root.isVisible = false
101+
} else {
102+
views.root.alpha = 0f
103+
views.root.isVisible = true
104+
views.root.animate().alpha(1f).setDuration(150).start()
105+
}
106+
}
107+
108+
private fun setupVoiceMessageView() {
109+
audioMessagePlaybackTracker.track(AudioMessagePlaybackTracker.RECORDING_ID, views.voiceMessageRecorderView)
110+
views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback {
111+
112+
override fun onVoiceRecordingStarted() {
113+
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
114+
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
115+
vibrate(requireContext())
116+
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Recording(clock.epochMillis()))
117+
}
118+
}
119+
120+
override fun onVoicePlaybackButtonClicked() {
121+
messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseRecordingPlayback)
122+
}
123+
124+
override fun onVoiceRecordingCancelled() {
125+
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId()))
126+
vibrate(requireContext())
127+
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Idle)
128+
}
129+
130+
override fun onVoiceRecordingLocked() {
131+
val startedState = withState(messageComposerViewModel) { it.voiceRecordingUiState as? VoiceMessageRecorderView.RecordingUiState.Recording }
132+
val startTime = startedState?.recordingStartTimestamp ?: clock.epochMillis()
133+
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Locked(startTime))
134+
}
135+
136+
override fun onVoiceRecordingEnded() {
137+
onSendVoiceMessage()
138+
}
139+
140+
override fun onSendVoiceMessage() {
141+
messageComposerViewModel.handle(
142+
MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false, rootThreadEventId = getRootThreadEventId())
143+
)
144+
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Idle)
145+
}
146+
147+
override fun onDeleteVoiceMessage() {
148+
messageComposerViewModel.handle(
149+
MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId())
150+
)
151+
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Idle)
152+
}
153+
154+
override fun onRecordingLimitReached() = pauseRecording()
155+
156+
override fun onRecordingWaveformClicked() = pauseRecording()
157+
158+
override fun onVoiceWaveformTouchedUp(percentage: Float, duration: Int) {
159+
messageComposerViewModel.handle(
160+
MessageComposerAction.VoiceWaveformTouchedUp(AudioMessagePlaybackTracker.RECORDING_ID, duration, percentage)
161+
)
162+
}
163+
164+
override fun onVoiceWaveformMoved(percentage: Float, duration: Int) {
165+
messageComposerViewModel.handle(
166+
MessageComposerAction.VoiceWaveformTouchedUp(AudioMessagePlaybackTracker.RECORDING_ID, duration, percentage)
167+
)
168+
}
169+
170+
private fun updateRecordingUiState(state: VoiceMessageRecorderView.RecordingUiState) {
171+
messageComposerViewModel.handle(
172+
MessageComposerAction.OnVoiceRecordingUiStateChanged(state)
173+
)
174+
}
175+
176+
private fun pauseRecording() {
177+
messageComposerViewModel.handle(
178+
MessageComposerAction.PauseRecordingVoiceMessage
179+
)
180+
updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Draft)
181+
}
182+
}
183+
}
184+
185+
/**
186+
* Returns the root thread event if we are in a thread room, otherwise returns null.
187+
*/
188+
fun getRootThreadEventId(): String? = withState(timelineViewModel) { it.rootThreadEventId }
189+
}

0 commit comments

Comments
 (0)