From ece62b797883f42f2f4ea108f2a935d28aa5a2ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2024 17:26:12 +0100 Subject: [PATCH 01/12] Allow caption for audio and file. Need to preview all the attachments now, to be able to type a caption. --- .../messages/impl/MessagesStateProvider.kt | 11 ----- .../features/messages/impl/MessagesView.kt | 15 ------ .../preview/AttachmentsPreviewState.kt | 8 +--- .../messagecomposer/MessageComposerEvents.kt | 1 - .../MessageComposerPresenter.kt | 46 +------------------ .../messagecomposer/MessageComposerState.kt | 4 -- .../libraries/matrix/api/room/MatrixRoom.kt | 16 ++++++- .../libraries/matrix/api/timeline/Timeline.kt | 16 ++++++- .../matrix/impl/room/RustMatrixRoom.kt | 32 +++++++++++-- .../matrix/impl/timeline/RustTimeline.kt | 29 +++++++++--- .../matrix/test/room/FakeMatrixRoom.kt | 16 +++++-- .../matrix/test/timeline/FakeTimeline.kt | 16 ++++++- .../libraries/mediaupload/api/MediaSender.kt | 4 ++ 13 files changed, 112 insertions(+), 102 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index e8dc5329f4..c3daabf3ed 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -12,7 +12,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState -import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState @@ -62,16 +61,6 @@ open class MessagesStateProvider : PreviewParameterProvider { enableVoiceMessages = true, voiceMessageComposerState = aVoiceMessageComposerState(showPermissionRationaleDialog = true), ), - aMessagesState( - composerState = aMessageComposerState( - attachmentsState = AttachmentsState.Sending.Processing(persistentListOf()) - ), - ), - aMessagesState( - composerState = aMessageComposerState( - attachmentsState = AttachmentsState.Sending.Uploading(0.33f) - ), - ), aMessagesState( roomCallState = anOngoingCallState(), ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 4351dbcae9..49cbac6268 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -83,8 +83,6 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.androidutils.ui.hideKeyboard import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule -import io.element.android.libraries.designsystem.components.ProgressDialog -import io.element.android.libraries.designsystem.components.ProgressDialogType import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar @@ -134,7 +132,6 @@ fun MessagesView( AttachmentStateView( state = state.composerState.attachmentsState, onPreviewAttachments = onPreviewAttachments, - onCancel = { state.composerState.eventSink(MessageComposerEvents.CancelSendAttachment) }, ) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) @@ -280,7 +277,6 @@ private fun ReinviteDialog(state: MessagesState) { private fun AttachmentStateView( state: AttachmentsState, onPreviewAttachments: (ImmutableList) -> Unit, - onCancel: () -> Unit, ) { when (state) { AttachmentsState.None -> Unit @@ -290,17 +286,6 @@ private fun AttachmentStateView( latestOnPreviewAttachments(state.attachments) } } - is AttachmentsState.Sending -> { - ProgressDialog( - type = when (state) { - is AttachmentsState.Sending.Uploading -> ProgressDialogType.Determinate(state.progress) - is AttachmentsState.Sending.Processing -> ProgressDialogType.Indeterminate - }, - text = stringResource(id = CommonStrings.common_sending), - showCancelButton = true, - onDismissRequest = onCancel, - ) - } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt index 5ffe9364ff..5fed2acfb2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt @@ -9,9 +9,6 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage -import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.textcomposer.model.TextEditorState data class AttachmentsPreviewState( @@ -20,9 +17,8 @@ data class AttachmentsPreviewState( val textEditorState: TextEditorState, val eventSink: (AttachmentsPreviewEvents) -> Unit ) { - val allowCaption: Boolean = (attachment as? Attachment.Media)?.localMedia?.info?.mimeType?.let { - it.isMimeTypeImage() || it.isMimeTypeVideo() - }.orFalse() + // Keep the val to eventually set to false for some mimetypes. + val allowCaption: Boolean = true } @Immutable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index fca9948339..036401bb1a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -31,7 +31,6 @@ sealed interface MessageComposerEvents { data object Poll : PickAttachmentSource } data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents - data object CancelSendAttachment : MessageComposerEvents data class Error(val error: Throwable) : MessageComposerEvents data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 6101f48c1f..7ad64c21d0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -42,7 +42,6 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.permalink.PermalinkData @@ -81,7 +80,6 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect @@ -89,11 +87,9 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -import kotlin.coroutines.coroutineContext import kotlin.time.Duration.Companion.seconds import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes @@ -180,26 +176,12 @@ class MessageComposerPresenter @Inject constructor( val isFullScreen = rememberSaveable { mutableStateOf(false) } - val ongoingSendAttachmentJob = remember { mutableStateOf(null) } - var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) } val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true) val roomAliasSuggestions by roomAliasSuggestionsDataSource.getAllRoomAliasSuggestions().collectAsState(initial = emptyList()) - LaunchedEffect(attachmentsState.value) { - when (val attachmentStateValue = attachmentsState.value) { - is AttachmentsState.Sending.Processing -> { - ongoingSendAttachmentJob.value = localCoroutineScope.sendAttachment( - attachmentStateValue.attachments.first(), - attachmentsState, - ) - } - else -> Unit - } - } - LaunchedEffect(cameraPermissionState.permissionGranted) { if (cameraPermissionState.permissionGranted) { when (pendingEvent) { @@ -338,12 +320,6 @@ class MessageComposerPresenter @Inject constructor( showAttachmentSourcePicker = false // Navigation to the create poll screen is done at the view layer } - is MessageComposerEvents.CancelSendAttachment -> { - ongoingSendAttachmentJob.value?.let { - it.cancel() - ongoingSendAttachmentJob.value == null - } - } is MessageComposerEvents.ToggleTextFormatting -> { showAttachmentSourcePicker = false localCoroutineScope.toggleTextFormatting(event.enabled, markdownTextEditorState, richTextEditorState) @@ -505,17 +481,7 @@ class MessageComposerPresenter @Inject constructor( formattedFileSize = null ) val mediaAttachment = Attachment.Media(localMedia) - val isPreviewable = when { - MimeTypes.isImage(localMedia.info.mimeType) -> true - MimeTypes.isVideo(localMedia.info.mimeType) -> true - MimeTypes.isAudio(localMedia.info.mimeType) -> true - else -> false - } - attachmentsState.value = if (isPreviewable) { - AttachmentsState.Previewing(persistentListOf(mediaAttachment)) - } else { - AttachmentsState.Sending.Processing(persistentListOf(mediaAttachment)) - } + attachmentsState.value = AttachmentsState.Previewing(persistentListOf(mediaAttachment)) } private suspend fun sendMedia( @@ -523,18 +489,10 @@ class MessageComposerPresenter @Inject constructor( mimeType: String, attachmentState: MutableState, ) = runCatching { - val context = coroutineContext - val progressCallback = object : ProgressCallback { - override fun onProgress(current: Long, total: Long) { - if (context.isActive) { - attachmentState.value = AttachmentsState.Sending.Uploading(current.toFloat() / total.toFloat()) - } - } - } mediaSender.sendMedia( uri = uri, mimeType = mimeType, - progressCallback = progressCallback + progressCallback = null, ).getOrThrow() } .onSuccess { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 5ca4a9c52b..d0e9528e48 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -35,8 +35,4 @@ data class MessageComposerState( sealed interface AttachmentsState { data object None : AttachmentsState data class Previewing(val attachments: ImmutableList) : AttachmentsState - sealed interface Sending : AttachmentsState { - data class Processing(val attachments: ImmutableList) : Sending - data class Uploading(val progress: Float) : Sending - } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 8dd1c5ec5f..989c301e92 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -147,9 +147,21 @@ interface MatrixRoom : Closeable { progressCallback: ProgressCallback? ): Result - suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result + suspend fun sendAudio( + file: File, + audioInfo: AudioInfo, + caption: String?, + formattedCaption: String?, + progressCallback: ProgressCallback?, + ): Result - suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result + suspend fun sendFile( + file: File, + fileInfo: FileInfo, + caption: String?, + formattedCaption: String?, + progressCallback: ProgressCallback?, + ): Result suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 695fe906c5..2b7c121dec 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -91,9 +91,21 @@ interface Timeline : AutoCloseable { suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result - suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result + suspend fun sendAudio( + file: File, + audioInfo: AudioInfo, + caption: String?, + formattedCaption: String?, + progressCallback: ProgressCallback?, + ): Result - suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result + suspend fun sendFile( + file: File, + fileInfo: FileInfo, + caption: String?, + formattedCaption: String?, + progressCallback: ProgressCallback?, + ): Result suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index c12fc17553..e3a7e2dfc7 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -467,12 +467,36 @@ class RustMatrixRoom( return liveTimeline.sendVideo(file, thumbnailFile, videoInfo, caption, formattedCaption, progressCallback) } - override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result { - return liveTimeline.sendAudio(file, audioInfo, progressCallback) + override suspend fun sendAudio( + file: File, + audioInfo: AudioInfo, + caption: String?, + formattedCaption: String?, + progressCallback: ProgressCallback?, + ): Result { + return liveTimeline.sendAudio( + file = file, + audioInfo = audioInfo, + caption = caption, + formattedCaption = formattedCaption, + progressCallback = progressCallback, + ) } - override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result { - return liveTimeline.sendFile(file, fileInfo, progressCallback) + override suspend fun sendFile( + file: File, + fileInfo: FileInfo, + caption: String?, + formattedCaption: String?, + progressCallback: ProgressCallback?, + ): Result { + return liveTimeline.sendFile( + file, + fileInfo, + caption, + formattedCaption, + progressCallback, + ) } override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 2b6d8543c4..813cc9c7cb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -373,29 +373,44 @@ class RustTimeline( } } - override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result { + override suspend fun sendAudio( + file: File, + audioInfo: AudioInfo, + caption: String?, + formattedCaption: String?, + progressCallback: ProgressCallback?, + ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOf(file)) { inner.sendAudio( url = file.path, audioInfo = audioInfo.map(), - // Maybe allow a caption in the future? - caption = null, - formattedCaption = null, + caption = caption, + formattedCaption = formattedCaption?.let { + FormattedBody(body = it, format = MessageFormat.Html) + }, useSendQueue = useSendQueue, progressWatcher = progressCallback?.toProgressWatcher() ) } } - override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result { + override suspend fun sendFile( + file: File, + fileInfo: FileInfo, + caption: String?, + formattedCaption: String?, + progressCallback: ProgressCallback?, + ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOf(file)) { inner.sendFile( url = file.path, fileInfo = fileInfo.map(), - caption = null, - formattedCaption = null, + caption = caption, + formattedCaption = formattedCaption?.let { + FormattedBody(body = it, format = MessageFormat.Html) + }, useSendQueue = useSendQueue, progressWatcher = progressCallback?.toProgressWatcher(), ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 9574a2a22c..9974e36746 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -92,10 +92,10 @@ class FakeMatrixRoom( { _, _, _, _, _, _ -> lambdaError() }, private val sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?) -> Result = { _, _, _, _, _, _ -> lambdaError() }, - private val sendFileResult: (File, FileInfo, ProgressCallback?) -> Result = - { _, _, _ -> lambdaError() }, - private val sendAudioResult: (File, AudioInfo, ProgressCallback?) -> Result = - { _, _, _ -> lambdaError() }, + private val sendFileResult: (File, FileInfo, String?, String?, ProgressCallback?) -> Result = + { _, _, _, _, _ -> lambdaError() }, + private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?) -> Result = + { _, _, _, _, _ -> lambdaError() }, private val sendVoiceMessageResult: (File, AudioInfo, List, ProgressCallback?) -> Result = { _, _, _, _ -> lambdaError() }, private val setNameResult: (String) -> Result = { lambdaError() }, @@ -354,12 +354,16 @@ class FakeMatrixRoom( override suspend fun sendAudio( file: File, audioInfo: AudioInfo, + caption: String?, + formattedCaption: String?, progressCallback: ProgressCallback? ): Result = simulateLongTask { simulateSendMediaProgress(progressCallback) sendAudioResult( file, audioInfo, + caption, + formattedCaption, progressCallback, ) } @@ -367,12 +371,16 @@ class FakeMatrixRoom( override suspend fun sendFile( file: File, fileInfo: FileInfo, + caption: String?, + formattedCaption: String?, progressCallback: ProgressCallback? ): Result = simulateLongTask { simulateSendMediaProgress(progressCallback) sendFileResult( file, fileInfo, + caption, + formattedCaption, progressCallback, ) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index ae40a0a51e..f96815c85a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -173,36 +173,48 @@ class FakeTimeline( var sendAudioLambda: ( file: File, audioInfo: AudioInfo, + caption: String?, + formattedCaption: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _ -> + ) -> Result = { _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } override suspend fun sendAudio( file: File, audioInfo: AudioInfo, + caption: String?, + formattedCaption: String?, progressCallback: ProgressCallback?, ): Result = sendAudioLambda( file, audioInfo, + caption, + formattedCaption, progressCallback ) var sendFileLambda: ( file: File, fileInfo: FileInfo, + caption: String?, + formattedCaption: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _ -> + ) -> Result = { _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } override suspend fun sendFile( file: File, fileInfo: FileInfo, + caption: String?, + formattedCaption: String?, progressCallback: ProgressCallback?, ): Result = sendFileLambda( file, fileInfo, + caption, + formattedCaption, progressCallback ) diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 09b8de4b41..77e6d021d5 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -125,6 +125,8 @@ class MediaSender @Inject constructor( sendAudio( file = uploadInfo.file, audioInfo = uploadInfo.audioInfo, + caption = caption, + formattedCaption = formattedCaption, progressCallback = progressCallback ) } @@ -140,6 +142,8 @@ class MediaSender @Inject constructor( sendFile( file = uploadInfo.file, fileInfo = uploadInfo.fileInfo, + caption = caption, + formattedCaption = formattedCaption, progressCallback = progressCallback ) } From f353ecdd45154296ad1c28eada8afaf554ec5fb0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2024 10:00:43 +0100 Subject: [PATCH 02/12] Render caption below audio and file Event in the timeline. --- .../event/TimelineItemAttachmentView.kt | 128 ++++++++++++++++++ .../components/event/TimelineItemAudioView.kt | 52 +------ .../components/event/TimelineItemFileView.kt | 52 +------ .../event/TimelineItemAudioContentProvider.kt | 11 +- .../event/TimelineItemFileContentProvider.kt | 7 +- 5 files changed, 155 insertions(+), 95 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt new file mode 100644 index 0000000000..2624a8b5e7 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout +import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData +import io.element.android.libraries.designsystem.theme.components.Text + +/** + * package-private, you should only use TimelineItemFileView and TimelineItemAudioView. + */ +@Composable +fun TimelineItemAttachmentView( + filename: String, + fileExtensionAndSize: String, + caption: String?, + onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, + modifier: Modifier = Modifier, + icon: (@Composable () -> Unit) = {}, +) { + Column( + modifier = modifier, + ) { + TimelineItemAttachmentHeaderView( + filename = filename, + fileExtensionAndSize = fileExtensionAndSize, + hasCaption = caption != null, + onContentLayoutChange = onContentLayoutChange, + icon = icon, + ) + if (caption != null) { + TimelineItemAttachmentCaptionView( + modifier = Modifier.padding(top = 4.dp), + caption = caption, + onContentLayoutChange = onContentLayoutChange, + ) + } + } +} + +@Composable +private fun TimelineItemAttachmentHeaderView( + filename: String, + fileExtensionAndSize: String, + hasCaption: Boolean, + onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, + modifier: Modifier = Modifier, + icon: (@Composable () -> Unit), +) { + val iconSize = 32.dp + val spacing = 8.dp + Row( + modifier = modifier, + ) { + Box( + modifier = Modifier + .size(iconSize) + .clip(CircleShape) + .background(ElementTheme.materialColors.background), + contentAlignment = Alignment.Center, + ) { + icon() + } + Spacer(Modifier.width(spacing)) + Column { + Text( + text = filename, + color = ElementTheme.materialColors.primary, + maxLines = 2, + style = ElementTheme.typography.fontBodyLgRegular, + overflow = TextOverflow.Ellipsis + ) + Text( + text = fileExtensionAndSize, + color = ElementTheme.materialColors.secondary, + style = ElementTheme.typography.fontBodySmRegular, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + onTextLayout = if (hasCaption) { + {} + } else { + ContentAvoidingLayout.measureLastTextLine( + onContentLayoutChange = onContentLayoutChange, + extraWidth = iconSize + spacing + ) + }, + ) + } + } +} + +@Composable +private fun TimelineItemAttachmentCaptionView( + caption: String, + onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier, + text = caption, + color = ElementTheme.materialColors.primary, + style = ElementTheme.typography.fontBodyLgRegular, + onTextLayout = ContentAvoidingLayout.measureLastTextLine( + onContentLayoutChange = onContentLayoutChange, + ) + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt index 23069a1fac..32e97eed58 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt @@ -7,32 +7,20 @@ package io.element.android.features.messages.impl.timeline.components.event -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.GraphicEq import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentProvider import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Text @Composable fun TimelineItemAudioView( @@ -40,18 +28,13 @@ fun TimelineItemAudioView( onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, ) { - val iconSize = 32.dp - val spacing = 8.dp - Row( + TimelineItemAttachmentView( + filename = content.filename, + fileExtensionAndSize = content.fileExtensionAndSize, + caption = content.caption, + onContentLayoutChange = onContentLayoutChange, modifier = modifier, - ) { - Box( - modifier = Modifier - .size(iconSize) - .clip(CircleShape) - .background(ElementTheme.materialColors.background), - contentAlignment = Alignment.Center, - ) { + icon = { Icon( imageVector = Icons.Outlined.GraphicEq, contentDescription = null, @@ -60,28 +43,7 @@ fun TimelineItemAudioView( .size(16.dp), ) } - Spacer(Modifier.width(spacing)) - Column { - Text( - text = content.bestDescription, - color = ElementTheme.materialColors.primary, - maxLines = 2, - style = ElementTheme.typography.fontBodyLgRegular, - overflow = TextOverflow.Ellipsis - ) - Text( - text = content.fileExtensionAndSize, - color = ElementTheme.materialColors.secondary, - style = ElementTheme.typography.fontBodySmRegular, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - onTextLayout = ContentAvoidingLayout.measureLastTextLine( - onContentLayoutChange = onContentLayoutChange, - extraWidth = iconSize + spacing - ) - ) - } - } + ) } @PreviewsDayNight diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt index dadfadc299..a3cdd7701e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt @@ -7,24 +7,13 @@ package io.element.android.features.messages.impl.timeline.components.event -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentProvider @@ -32,7 +21,6 @@ import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Text @Composable fun TimelineItemFileView( @@ -40,18 +28,13 @@ fun TimelineItemFileView( onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, ) { - val iconSize = 32.dp - val spacing = 8.dp - Row( + TimelineItemAttachmentView( + filename = content.filename, + fileExtensionAndSize = content.fileExtensionAndSize, + caption = content.caption, + onContentLayoutChange = onContentLayoutChange, modifier = modifier, - ) { - Box( - modifier = Modifier - .size(iconSize) - .clip(CircleShape) - .background(ElementTheme.materialColors.background), - contentAlignment = Alignment.Center, - ) { + icon = { Icon( resourceId = CompoundDrawables.ic_compound_attachment, contentDescription = null, @@ -61,28 +44,7 @@ fun TimelineItemFileView( .rotate(-45f), ) } - Spacer(Modifier.width(spacing)) - Column { - Text( - text = content.bestDescription, - color = ElementTheme.materialColors.primary, - maxLines = 2, - style = ElementTheme.typography.fontBodyLgRegular, - overflow = TextOverflow.Ellipsis - ) - Text( - text = content.fileExtensionAndSize, - color = ElementTheme.materialColors.secondary, - style = ElementTheme.typography.fontBodySmRegular, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - onTextLayout = ContentAvoidingLayout.measureLastTextLine( - onContentLayoutChange = onContentLayoutChange, - extraWidth = iconSize + spacing - ) - ) - } - } + ) } @PreviewsDayNight diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt index 41dab0cfb6..d3ca18e836 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt @@ -17,13 +17,18 @@ open class TimelineItemAudioContentProvider : PreviewParameterProvider Date: Tue, 19 Nov 2024 11:55:46 +0100 Subject: [PATCH 03/12] Edit / Add / Remove caption --- .../messages/impl/MessagesPresenter.kt | 40 +++++++ .../impl/actionlist/ActionListPresenter.kt | 11 ++ .../actionlist/model/TimelineItemAction.kt | 3 + .../MessageComposerPresenter.kt | 27 ++++- .../libraries/matrix/api/timeline/Timeline.kt | 9 +- .../matrix/impl/timeline/RustTimeline.kt | 48 +++++--- .../matrix/test/timeline/FakeTimeline.kt | 18 +++ .../textcomposer/ComposerModeView.kt | 15 ++- .../libraries/textcomposer/TextComposer.kt | 112 +++++++++++++----- .../textcomposer/components/SendButton.kt | 12 +- .../textcomposer/model/MessageComposerMode.kt | 15 +-- .../src/main/res/values/localazy.xml | 5 + 12 files changed, 246 insertions(+), 69 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 7840c307c7..49d3015c05 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent @@ -273,6 +274,9 @@ class MessagesPresenter @AssistedInject constructor( TimelineItemAction.CopyLink -> handleCopyLink(targetEvent) TimelineItemAction.Redact -> handleActionRedact(targetEvent) TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting) + TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState) + TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState) + TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent) TimelineItemAction.Reply, TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState, timelineProtectionState) TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent) @@ -285,6 +289,16 @@ class MessagesPresenter @AssistedInject constructor( } } + private suspend fun handleRemoveCaption(targetEvent: TimelineItem.Event) { + timelineController.invokeOnCurrentTimeline { + editCaption( + eventOrTransactionId = targetEvent.eventOrTransactionId, + caption = null, + formattedCaption = null, + ) + } + } + private suspend fun handlePinAction(targetEvent: TimelineItem.Event) { if (targetEvent.eventId == null) return analyticsService.capture( @@ -387,6 +401,32 @@ class MessagesPresenter @AssistedInject constructor( } } + private fun handleActionAddCaption( + targetEvent: TimelineItem.Event, + composerState: MessageComposerState, + ) { + val composerMode = MessageComposerMode.EditCaption( + eventOrTransactionId = targetEvent.eventOrTransactionId, + content = "", + ) + composerState.eventSink( + MessageComposerEvents.SetMode(composerMode) + ) + } + + private fun handleActionEditCaption( + targetEvent: TimelineItem.Event, + composerState: MessageComposerState, + ) { + val composerMode = MessageComposerMode.EditCaption( + eventOrTransactionId = targetEvent.eventOrTransactionId, + content = (targetEvent.content as? TimelineItemEventContentWithAttachment)?.caption.orEmpty(), + ) + composerState.eventSink( + MessageComposerEvents.SetMode(composerMode) + ) + } + private suspend fun handleActionReply( targetEvent: TimelineItem.Event, composerState: MessageComposerState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 39e6cd8836..7bd4668ed4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent @@ -154,6 +155,16 @@ class DefaultActionListPresenter @AssistedInject constructor( } if (timelineItem.isEditable) { add(TimelineItemAction.Edit) + } else { + // Caption + if (timelineItem.isMine && timelineItem.content is TimelineItemEventContentWithAttachment) { + if (timelineItem.content.caption == null) { + add(TimelineItemAction.AddCaption) + } else { + add(TimelineItemAction.EditCaption) + add(TimelineItemAction.RemoveCaption) + } + } } if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) { add(TimelineItemAction.EndPoll) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index cddf3ff8e4..979f041297 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -28,6 +28,9 @@ sealed class TimelineItemAction( data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply) data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply) data object Edit : TimelineItemAction(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit) + data object EditCaption : TimelineItemAction(CommonStrings.action_edit_caption, CompoundDrawables.ic_compound_edit) + data object AddCaption : TimelineItemAction(CommonStrings.action_add_caption, CompoundDrawables.ic_compound_edit) + data object RemoveCaption : TimelineItemAction(CommonStrings.action_remove_caption, CompoundDrawables.ic_compound_delete, destructive = true) data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options) data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true) data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 7ad64c21d0..ebccc8dff4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -254,7 +254,7 @@ class MessageComposerPresenter @Inject constructor( when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value MessageComposerEvents.CloseSpecialMode -> { - if (messageComposerContext.composerMode is MessageComposerMode.Edit) { + if (messageComposerContext.composerMode.isEditing) { localCoroutineScope.launch { resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = true) } @@ -431,7 +431,15 @@ class MessageComposerPresenter @Inject constructor( } } } - + is MessageComposerMode.EditCaption -> { + timelineController.invokeOnCurrentTimeline { + editCaption( + capturedMode.eventOrTransactionId, + caption = message.markdown, + formattedCaption = message.html + ) + } + } is MessageComposerMode.Reply -> { timelineController.invokeOnCurrentTimeline { replyMessage(capturedMode.eventId, message.markdown, message.html, message.intentionalMentions) @@ -570,6 +578,10 @@ class MessageComposerPresenter @Inject constructor( mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) } } is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId) + is MessageComposerMode.EditCaption -> { + // TODO Need a new type to save caption in the SDK + null + } } return if (draftType == null || message.markdown.isBlank()) { null @@ -644,7 +656,14 @@ class MessageComposerPresenter @Inject constructor( val currentComposerMode = messageComposerContext.composerMode when (newComposerMode) { is MessageComposerMode.Edit -> { - if (currentComposerMode !is MessageComposerMode.Edit) { + if (currentComposerMode.isEditing.not()) { + val draft = createDraftFromState(markdownTextEditorState, richTextEditorState) + updateDraft(draft, isVolatile = true).join() + } + setText(newComposerMode.content, markdownTextEditorState, richTextEditorState) + } + is MessageComposerMode.EditCaption -> { + if (currentComposerMode.isEditing.not()) { val draft = createDraftFromState(markdownTextEditorState, richTextEditorState) updateDraft(draft, isVolatile = true).join() } @@ -652,7 +671,7 @@ class MessageComposerPresenter @Inject constructor( } else -> { // When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario. - if (currentComposerMode is MessageComposerMode.Edit) { + if (currentComposerMode.isEditing) { setText("", markdownTextEditorState, richTextEditorState) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 2b7c121dec..00f7a9a17c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -59,10 +59,17 @@ interface Timeline : AutoCloseable { suspend fun editMessage( eventOrTransactionId: EventOrTransactionId, - body: String, htmlBody: String?, + body: String, + htmlBody: String?, intentionalMentions: List, ): Result + suspend fun editCaption( + eventOrTransactionId: EventOrTransactionId, + caption: String?, + formattedCaption: String?, + ): Result + suspend fun replyMessage( eventId: EventId, body: String, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 813cc9c7cb..200f1289b4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -295,22 +295,40 @@ class RustTimeline( body: String, htmlBody: String?, intentionalMentions: List, - ): Result = - withContext(dispatcher) { - runCatching { - val editedContent = EditedContent.RoomMessage( - content = MessageEventContent.from( - body = body, - htmlBody = htmlBody, - intentionalMentions = intentionalMentions - ), - ) - inner.edit( - newContent = editedContent, - eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(), - ) - } + ): Result = withContext(dispatcher) { + runCatching { + val editedContent = EditedContent.RoomMessage( + content = MessageEventContent.from( + body = body, + htmlBody = htmlBody, + intentionalMentions = intentionalMentions + ), + ) + inner.edit( + newContent = editedContent, + eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(), + ) } + } + + override suspend fun editCaption( + eventOrTransactionId: EventOrTransactionId, + caption: String?, + formattedCaption: String?, + ): Result = withContext(dispatcher) { + runCatching { + val editedContent = EditedContent.MediaCaption( + caption = caption, + formattedCaption = formattedCaption?.let { + FormattedBody(body = it, format = MessageFormat.Html) + }, + ) + inner.edit( + newContent = editedContent, + eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(), + ) + } + } override suspend fun replyMessage( eventId: EventId, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index f96815c85a..6edfd22c50 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -92,6 +92,24 @@ class FakeTimeline( intentionalMentions ) + var editCaptionLambda: ( + eventOrTransactionId: EventOrTransactionId, + caption: String?, + formattedCaption: String?, + ) -> Result = { _, _, _ -> + lambdaError() + } + + override suspend fun editCaption( + eventOrTransactionId: EventOrTransactionId, + caption: String?, + formattedCaption: String?, + ): Result = editCaptionLambda( + eventOrTransactionId, + caption, + formattedCaption, + ) + var replyMessageLambda: ( eventId: EventId, body: String, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt index 218a8653cf..eaa5669206 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt @@ -47,6 +47,16 @@ internal fun ComposerModeView( when (composerMode) { is MessageComposerMode.Edit -> { EditingModeView( + text = stringResource(CommonStrings.common_editing), + modifier = modifier, + onResetComposerMode = onResetComposerMode, + ) + } + is MessageComposerMode.EditCaption -> { + EditingModeView( + text = stringResource( + if (composerMode.content.isEmpty()) CommonStrings.common_adding_caption else CommonStrings.common_editing_caption + ), modifier = modifier, onResetComposerMode = onResetComposerMode, ) @@ -65,6 +75,7 @@ internal fun ComposerModeView( @Composable private fun EditingModeView( onResetComposerMode: () -> Unit, + text: String, modifier: Modifier = Modifier, ) { Row( @@ -76,14 +87,14 @@ private fun EditingModeView( ) { Icon( imageVector = CompoundIcons.Edit(), - contentDescription = stringResource(CommonStrings.common_editing), + contentDescription = null, tint = ElementTheme.materialColors.secondary, modifier = Modifier .padding(vertical = 8.dp) .size(16.dp), ) Text( - stringResource(CommonStrings.common_editing), + text = text, style = ElementTheme.typography.fontBodySmRegular, textAlign = TextAlign.Start, color = ElementTheme.materialColors.secondary, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index eca7340219..81cc7a739f 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -121,19 +121,25 @@ fun TextComposer( } val layoutModifier = modifier - .fillMaxSize() - .height(IntrinsicSize.Min) + .fillMaxSize() + .height(IntrinsicSize.Min) - val composerOptionsButton: @Composable () -> Unit = remember { + val composerOptionsButton: @Composable () -> Unit = remember(composerMode) { @Composable { - if (composerMode is MessageComposerMode.Attachment) { - Spacer(modifier = Modifier.width(9.dp)) - } else { - ComposerOptionsButton( - modifier = Modifier - .size(48.dp), - onClick = onAddAttachment - ) + when (composerMode) { + is MessageComposerMode.Attachment -> { + Spacer(modifier = Modifier.width(9.dp)) + } + is MessageComposerMode.EditCaption -> { + Spacer(modifier = Modifier.width(16.dp)) + } + else -> { + ComposerOptionsButton( + modifier = Modifier + .size(48.dp), + onClick = onAddAttachment + ) + } } } } @@ -331,8 +337,8 @@ private fun StandardLayout( if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) { Box( modifier = Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) - .size(48.dp), + .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) + .size(48.dp), contentAlignment = Alignment.Center, ) { voiceDeleteButton() @@ -342,8 +348,8 @@ private fun StandardLayout( } Box( modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) ) { voiceRecording() } @@ -356,16 +362,16 @@ private fun StandardLayout( } Box( modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) ) { textInput() } } Box( - Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) - .size(48.dp), + Modifier + .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) + .size(48.dp), contentAlignment = Alignment.Center, ) { endButton() @@ -387,8 +393,8 @@ private fun TextFormattingLayout( ) { Box( modifier = Modifier - .weight(1f) - .padding(horizontal = 12.dp) + .weight(1f) + .padding(horizontal = 12.dp) ) { textInput() } @@ -432,11 +438,11 @@ private fun TextInputBox( Column( modifier = Modifier - .clip(roundedCorners) - .border(0.5.dp, borderColor, roundedCorners) - .background(color = bgColor) - .requiredHeightIn(min = 42.dp) - .fillMaxSize(), + .clip(roundedCorners) + .border(0.5.dp, borderColor, roundedCorners) + .background(color = bgColor) + .requiredHeightIn(min = 42.dp) + .fillMaxSize(), ) { if (composerMode is MessageComposerMode.Special) { ComposerModeView( @@ -447,9 +453,9 @@ private fun TextInputBox( val defaultTypography = ElementTheme.typography.fontBodyLgRegular Box( modifier = Modifier - .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp) - // Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail - .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), + .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp) + // Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail + .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), contentAlignment = Alignment.CenterStart, ) { // Placeholder @@ -495,8 +501,8 @@ private fun TextInput( // This prevents it gaining focus and mutating the state. registerStateUpdates = !subcomposing, modifier = Modifier - .padding(top = 6.dp, bottom = 6.dp) - .fillMaxWidth(), + .padding(top = 6.dp, bottom = 6.dp) + .fillMaxWidth(), style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus), resolveMentionDisplay = resolveMentionDisplay, resolveRoomMentionDisplay = resolveRoomMentionDisplay, @@ -573,6 +579,40 @@ internal fun TextComposerEditPreview() = ElementPreview { } } +@PreviewsDayNight +@Composable +internal fun TextComposerEditCaptionPreview() = ElementPreview { + PreviewColumn( + items = aTextEditorStateRichList() + ) { _, textEditorState -> + ATextComposer( + state = textEditorState, + voiceMessageState = VoiceMessageState.Idle, + composerMode = aMessageComposerModeEditCaption( + content = "A caption", + ), + enableVoiceMessages = false, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun TextComposerAddCaptionPreview() = ElementPreview { + PreviewColumn( + items = aTextEditorStateRichList() + ) { _, textEditorState -> + ATextComposer( + state = textEditorState, + voiceMessageState = VoiceMessageState.Idle, + composerMode = aMessageComposerModeEditCaption( + content = "", + ), + enableVoiceMessages = false, + ) + } +} + @PreviewsDayNight @Composable internal fun MarkdownTextComposerEditPreview() = ElementPreview { @@ -717,6 +757,14 @@ fun aMessageComposerModeEdit( content = content ) +fun aMessageComposerModeEditCaption( + eventOrTransactionId: EventOrTransactionId = EventId("$1234").toEventOrTransactionId(), + content: String = "Some caption", +) = MessageComposerMode.EditCaption( + eventOrTransactionId = eventOrTransactionId, + content = content +) + fun aMessageComposerModeReply( replyToDetails: InReplyToDetails, hideImage: Boolean = false, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt index 6b5d601333..b7b0368aeb 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt @@ -53,16 +53,16 @@ internal fun SendButton( onClick = onClick, enabled = canSendMessage, ) { - val iconVector = when (composerMode) { - is MessageComposerMode.Edit -> CompoundIcons.Check() + val iconVector = when { + composerMode.isEditing -> CompoundIcons.Check() else -> CompoundIcons.SendSolid() } - val iconStartPadding = when (composerMode) { - is MessageComposerMode.Edit -> 0.dp + val iconStartPadding = when { + composerMode.isEditing -> 0.dp else -> 2.dp } - val contentDescription = when (composerMode) { - is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit) + val contentDescription = when { + composerMode.isEditing -> stringResource(CommonStrings.action_edit) else -> stringResource(CommonStrings.action_send) } Box( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index 1915359c83..4ef6cfc019 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -27,6 +27,11 @@ sealed interface MessageComposerMode { val content: String ) : Special + data class EditCaption( + val eventOrTransactionId: EventOrTransactionId, + val content: String + ) : Special + data class Reply( val replyToDetails: InReplyToDetails, val hideImage: Boolean, @@ -34,16 +39,8 @@ sealed interface MessageComposerMode { val eventId: EventId = replyToDetails.eventId() } - val relatedEventId: EventId? - get() = when (this) { - is Normal, - is Attachment -> null - is Edit -> eventOrTransactionId.eventId - is Reply -> eventId - } - val isEditing: Boolean - get() = this is Edit + get() = this is Edit || this is EditCaption val isReply: Boolean get() = this is Reply diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index ab3a93a692..2245657b6b 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -32,6 +32,7 @@ "Record voice message." "Stop recording" "Accept" + "Add caption" "Add to timeline" "Back" "Call" @@ -57,6 +58,7 @@ "Discard" "Done" "Edit" + "Edit caption" "Edit poll" "Enable" "End poll" @@ -91,6 +93,7 @@ "React" "Reject" "Remove" + "Remove caption" "Reply" "Reply in thread" "Report bug" @@ -123,6 +126,7 @@ "Yes" "About" "Acceptable use policy" + "Adding caption" "Advanced settings" "Analytics" "Appearance" @@ -143,6 +147,7 @@ "Do not show this again" "(edited)" "Editing" + "Editing caption" "* %1$s %2$s" "Encryption" "Encryption enabled" From f3e0a4dc00327adf6b51d20627c10a26d5ead9b3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2024 14:56:15 +0100 Subject: [PATCH 04/12] Add matrix_sdk::send_queue to be able to configure it. --- .../libraries/matrix/api/tracing/TracingFilterConfiguration.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt index ec80d320c5..8516401350 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt @@ -57,6 +57,7 @@ enum class Target(open val filter: String) { MATRIX_SDK_HTTP_CLIENT("matrix_sdk::http_client"), MATRIX_SDK_CLIENT("matrix_sdk::client"), MATRIX_SDK_OIDC("matrix_sdk::oidc"), + MATRIX_SDK_SEND_QUEUE("matrix_sdk::send_queue"), MATRIX_SDK_SLIDING_SYNC("matrix_sdk::sliding_sync"), MATRIX_SDK_BASE_SLIDING_SYNC("matrix_sdk_base::sliding_sync"), MATRIX_SDK_UI_TIMELINE("matrix_sdk_ui::timeline"), From d6ee0846b33b028909116ca673aef2218c79ce53 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2024 17:32:41 +0100 Subject: [PATCH 05/12] Fix tests --- .../impl/actionlist/ActionListPresenter.kt | 5 +- .../actionlist/ActionListPresenterTest.kt | 53 ++++++++++++++- .../AttachmentsPreviewPresenterTest.kt | 4 +- .../MessageComposerPresenterTest.kt | 64 +------------------ .../features/share/impl/SharePresenterTest.kt | 2 +- .../mediaupload/api/MediaSenderTest.kt | 2 +- .../tests/konsist/KonsistPreviewTest.kt | 2 + 7 files changed, 63 insertions(+), 69 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 7bd4668ed4..2ae72c86c6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.canBeCopied import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded import io.element.android.features.messages.impl.timeline.model.event.canReact @@ -157,7 +158,9 @@ class DefaultActionListPresenter @AssistedInject constructor( add(TimelineItemAction.Edit) } else { // Caption - if (timelineItem.isMine && timelineItem.content is TimelineItemEventContentWithAttachment) { + if (timelineItem.isMine && + timelineItem.content is TimelineItemEventContentWithAttachment && + timelineItem.content !is TimelineItemVoiceContent) { if (timelineItem.content.caption == null) { add(TimelineItemAction.AddCaption) } else { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 078fb9e676..98d6ad0ec7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -29,6 +29,7 @@ import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_CAPTION import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom @@ -444,8 +445,6 @@ class ActionListPresenterTest { ), ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -455,6 +454,56 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, + TimelineItemAction.AddCaption, + TimelineItemAction.Pin, + TimelineItemAction.CopyLink, + TimelineItemAction.ViewSource, + TimelineItemAction.Redact, + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + + @Test + fun `present - compute for a media with caption item`() = runTest { + val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + isEditable = false, + content = aTimelineItemImageContent( + caption = A_CAPTION, + ), + ) + initialState.eventSink.invoke( + ActionListEvents.ComputeForMessage( + event = messageEvent, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + canPinUnpin = true, + ), + ) + ) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + event = messageEvent, + displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, + actions = persistentListOf( + TimelineItemAction.Reply, + TimelineItemAction.Forward, + TimelineItemAction.EditCaption, + TimelineItemAction.RemoveCaption, TimelineItemAction.Pin, TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt index ac753f6cdb..bc61728cb1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt @@ -60,7 +60,7 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media success scenario`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _ -> + val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val room = FakeMatrixRoom( @@ -192,7 +192,7 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media failure scenario`() = runTest { val failure = MediaPreProcessor.Failure(null) - val sendFileResult = lambdaRecorder> { _, _, _ -> + val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> Result.failure(failure) } val room = FakeMatrixRoom( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index 2e68c9b199..c1d1fd8c11 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -30,9 +30,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder @@ -58,7 +56,6 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_3 import io.element.android.libraries.matrix.test.A_USER_ID_4 import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom @@ -684,17 +681,8 @@ class MessageComposerPresenterTest { } @Test - fun `present - Pick file from storage`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } + fun `present - Pick file from storage will open the preview`() = runTest { val room = FakeMatrixRoom( - progressCallbackValues = listOf( - Pair(0, 10), - Pair(5, 10), - Pair(10, 10) - ), - sendFileResult = sendFileResult, typingNoticeResult = { Result.success(Unit) } ) val presenter = createPresenter(this, room = room) @@ -705,13 +693,7 @@ class MessageComposerPresenterTest { initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) val sendingState = awaitItem() assertThat(sendingState.showAttachmentSourcePicker).isFalse() - assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending.Processing::class.java) - assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(0f)) - assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(0.5f)) - assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(1f)) - val sentState = awaitItem() - assertThat(sentState.attachmentsState).isEqualTo(AttachmentsState.None) - sendFileResult.assertions().isCalledOnce() + assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java) } } @@ -851,48 +833,6 @@ class MessageComposerPresenterTest { } } - @Test - fun `present - Uploading media failure can be recovered from`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _ -> - Result.failure(Exception()) - } - val room = FakeMatrixRoom( - sendFileResult = sendFileResult, - typingNoticeResult = { Result.success(Unit) } - ) - val presenter = createPresenter(this, room = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitFirstItem() - initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) - val sendingState = awaitItem() - assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java) - val finalState = awaitItem() - assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.None::class.java) - snackbarDispatcher.snackbarMessage.test { - // Assert error message received - assertThat(awaitItem()).isNotNull() - } - } - } - - @Test - fun `present - CancelSendAttachment stops media upload`() = runTest { - val presenter = createPresenter(this) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitFirstItem() - initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) - val sendingState = awaitItem() - assertThat(sendingState.showAttachmentSourcePicker).isFalse() - assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending.Processing::class.java) - sendingState.eventSink(MessageComposerEvents.CancelSendAttachment) - assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.None) - } - } - @Test fun `present - errors are tracked`() = runTest { val testException = Exception("Test error") diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt index 10d42716a9..e59e2d74c8 100644 --- a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt +++ b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt @@ -116,7 +116,7 @@ class SharePresenterTest { @Test fun `present - send media ok`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _ -> + val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val matrixRoom = FakeMatrixRoom( diff --git a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt index 1cfb46c20e..39f139afec 100644 --- a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt +++ b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt @@ -91,7 +91,7 @@ class MediaSenderTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `given a cancellation in the media upload when sending the job is cancelled`() = runTest(StandardTestDispatcher()) { - val sendFileResult = lambdaRecorder> { _, _, _ -> + val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val room = FakeMatrixRoom( diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 83541624a1..8d26082157 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -101,8 +101,10 @@ class KonsistPreviewTest { "SasEmojisPreview", "SecureBackupSetupViewChangePreview", "SelectedUserCannotRemovePreview", + "TextComposerAddCaptionPreview", "TextComposerCaptionPreview", "TextComposerEditPreview", + "TextComposerEditCaptionPreview", "TextComposerFormattingPreview", "TextComposerLinkDialogCreateLinkPreview", "TextComposerLinkDialogCreateLinkWithoutTextPreview", From e0964299c36b12427573f6e4bc5efb8f3ffc5a68 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2024 18:02:07 +0100 Subject: [PATCH 06/12] Add missing test to check for TimelineItemAction.ReplyInThread --- .../actionlist/ActionListPresenterTest.kt | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 98d6ad0ec7..463e5108c7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -185,6 +186,51 @@ class ActionListPresenterTest { } } + @Test + fun `present - compute for others message in a thread`() = runTest { + val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true) + presenter.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = false, + isEditable = false, + isThreaded = true, + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + ) + initialState.eventSink.invoke( + ActionListEvents.ComputeForMessage( + event = messageEvent, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + canPinUnpin = true, + ) + ) + ) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + event = messageEvent, + displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, + actions = persistentListOf( + TimelineItemAction.ReplyInThread, + TimelineItemAction.Forward, + TimelineItemAction.Pin, + TimelineItemAction.Copy, + TimelineItemAction.CopyLink, + TimelineItemAction.ViewSource, + TimelineItemAction.ReportContent, + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + @Test fun `present - compute for others message cannot sent message`() = runTest { val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true) @@ -374,6 +420,51 @@ class ActionListPresenterTest { } } + @Test + fun `present - compute for my message in a thread`() = runTest { + val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true) + presenter.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + isThreaded = true, + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + ) + initialState.eventSink.invoke( + ActionListEvents.ComputeForMessage( + event = messageEvent, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + canPinUnpin = true, + ) + ) + ) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + event = messageEvent, + displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, + actions = persistentListOf( + TimelineItemAction.ReplyInThread, + TimelineItemAction.Forward, + TimelineItemAction.Edit, + TimelineItemAction.Pin, + TimelineItemAction.Copy, + TimelineItemAction.CopyLink, + TimelineItemAction.ViewSource, + TimelineItemAction.Redact, + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + @Test fun `present - compute for my message cannot redact`() = runTest { val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true) From 8bcc9203ad4ab017ff8f0391c07a0160e3f05b22 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 20 Nov 2024 13:10:52 +0000 Subject: [PATCH 07/12] Update screenshots --- ...messages.impl.attachments.preview_AttachmentsView_2_en.png | 4 ++-- ...messages.impl.attachments.preview_AttachmentsView_3_en.png | 4 ++-- ...meline.components.event_TimelineItemAudioView_Day_3_en.png | 3 +++ ...meline.components.event_TimelineItemAudioView_Day_4_en.png | 3 +++ ...line.components.event_TimelineItemAudioView_Night_3_en.png | 3 +++ ...line.components.event_TimelineItemAudioView_Night_4_en.png | 3 +++ ...imeline.components.event_TimelineItemFileView_Day_2_en.png | 4 ++-- ...imeline.components.event_TimelineItemFileView_Day_3_en.png | 3 +++ ...imeline.components.event_TimelineItemFileView_Day_4_en.png | 3 +++ ...eline.components.event_TimelineItemFileView_Night_2_en.png | 4 ++-- ...eline.components.event_TimelineItemFileView_Night_3_en.png | 3 +++ ...eline.components.event_TimelineItemFileView_Night_4_en.png | 3 +++ .../images/features.messages.impl_MessagesView_Day_10_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_11_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_12_en.png | 3 --- .../images/features.messages.impl_MessagesView_Day_13_en.png | 3 --- .../images/features.messages.impl_MessagesView_Day_8_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_9_en.png | 4 ++-- .../features.messages.impl_MessagesView_Night_10_en.png | 4 ++-- .../features.messages.impl_MessagesView_Night_11_en.png | 4 ++-- .../features.messages.impl_MessagesView_Night_12_en.png | 3 --- .../features.messages.impl_MessagesView_Night_13_en.png | 3 --- .../images/features.messages.impl_MessagesView_Night_8_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_9_en.png | 4 ++-- ...libraries.textcomposer_TextComposerAddCaption_Day_0_en.png | 3 +++ ...braries.textcomposer_TextComposerAddCaption_Night_0_en.png | 3 +++ ...ibraries.textcomposer_TextComposerEditCaption_Day_0_en.png | 3 +++ ...raries.textcomposer_TextComposerEditCaption_Night_0_en.png | 3 +++ 28 files changed, 60 insertions(+), 36 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png index c94414246c..2cb5dc89f8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e0ec7e2a9dbe70a2deddf2bd621ac721b03e1b5bd367020fc68485e4d387ba8 -size 16610 +oid sha256:3250fa9767aec264308719c6fdf6cea94d5ccea21bfa8a735d7df5d79b572555 +size 21214 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png index 0d2dcbf67c..e34eb6e69c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a44e40c0d586b41a0e47aa7d40a564eb820c2692051e795c87c460df92e81ca6 -size 17739 +oid sha256:4ba70221a149ce0f8abaa4e4f870011bea82630edf99ecacea560a737f56346f +size 22215 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png new file mode 100644 index 0000000000..c54fbf3183 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d2c41387ac8ac6f06845e584abda8c7cdfdf59c5d969d8611724222d85d061b +size 10746 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png new file mode 100644 index 0000000000..05c8b7f2cd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08183886dd8ecc5e402ab02cfe798fd23153c5ebed7439563f070d0a84ebed10 +size 21131 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png new file mode 100644 index 0000000000..8bfb865969 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f96ad2f8c31fed8d0bbd8c4cedd2da39535cd45bb143d6fd10f2e92c470b3b4 +size 10809 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png new file mode 100644 index 0000000000..3675de2ae9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:151eb0a92ab7769d997bc3a459c67bb5cc04beac48cce5af7ee3867d46cbdbc0 +size 20849 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png index 10ca4fc77f..4c80d9632c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78ce6620309ead376b0be9a5e33da0ac935f0d6cc243d45ecfc3c69115388110 -size 21248 +oid sha256:727085653ecf863579d7fc952bf1d09aab8a41548e087435c1dd7b2b82350910 +size 21484 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png new file mode 100644 index 0000000000..b3e9b8f871 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8989d1bd6505db4067a8438963deb6bc8858d2a4b6c5d669df036ebca60e539f +size 10163 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png new file mode 100644 index 0000000000..299ad8ac2f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:518c57dfe5e6f5be40e32922808bb3eabd17f6aed525a5e62206dfee8bb14b13 +size 20576 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png index 218003b72f..5f7f2f5d51 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c66fb54a0b0f1c5fc30f2ff0be7a6f5e7d9bdc426f058825495a5c5194cffea6 -size 20763 +oid sha256:3cfd7c3d4fa0a2a289a15438092cb272d57be91ea221d73865a04b5ddf58b5ed +size 20996 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png new file mode 100644 index 0000000000..90ce08f533 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5745d06abd77e48b77fc192b911bcc60ed124af423d87c213fad67733bbb2943 +size 10113 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png new file mode 100644 index 0000000000..e446ff389d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:905386dfc27904f4cf36d26346e7a3dcafbf426ef4a1ac80717a45d1da0dd7d7 +size 20174 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png index 5779f0e9b3..2c917a94f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ce4299e4c585abf3635b37c6003ac50a8ed25ffa0e9258dfbc07d61fb04569f -size 61074 +oid sha256:7d1ed9a282d3363fa4e28d924a750d0a46f7564533f687ab102d559e2dd1606b +size 59518 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png index 67315b78fe..2f6c08ed16 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:035d793ee33682785b98974c6357b99bfb879f8a86cba7117bb2b9c4c4bc13d5 -size 49533 +oid sha256:9660c7ea6ca5101c6b5fa9e774d4063d4011b9f8fdb64bf8e32cbc89c231531c +size 62605 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png deleted file mode 100644 index 2c917a94f2..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7d1ed9a282d3363fa4e28d924a750d0a46f7564533f687ab102d559e2dd1606b -size 59518 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png deleted file mode 100644 index 2f6c08ed16..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9660c7ea6ca5101c6b5fa9e774d4063d4011b9f8fdb64bf8e32cbc89c231531c -size 62605 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png index 6f19d0f9d7..5779f0e9b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ad0d2f2e326fd05997ef115e31971727067543c543883029411b735ce8909ff -size 40004 +oid sha256:3ce4299e4c585abf3635b37c6003ac50a8ed25ffa0e9258dfbc07d61fb04569f +size 61074 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png index 44ec95c3e1..67315b78fe 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:281639101ede7b20bee08efdeac8c346fd13f9a9c4e3f322b6a6f37a417647b4 -size 39953 +oid sha256:035d793ee33682785b98974c6357b99bfb879f8a86cba7117bb2b9c4c4bc13d5 +size 49533 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png index 39c5faa542..eee7da7dda 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e9412f3e84b63be532188e13a976d1954404466136633f3f8cd41851617e753 -size 60668 +oid sha256:76faea3ec5b9e01a7e1ad77dea4f44b060bee26cf1d0b56df8eabb692cf764df +size 59225 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png index df384039ee..8643a775a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1da55e4b116cbf0ebd74afee2a47d34b99d87192cdf9a5f8698118fcd4bbe239 -size 45466 +oid sha256:0f9df03bc6cba3fbf8ae454768064c9141b05e319c40c568a0a3edf3a53fed4e +size 61961 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png deleted file mode 100644 index eee7da7dda..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:76faea3ec5b9e01a7e1ad77dea4f44b060bee26cf1d0b56df8eabb692cf764df -size 59225 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png deleted file mode 100644 index 8643a775a1..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0f9df03bc6cba3fbf8ae454768064c9141b05e319c40c568a0a3edf3a53fed4e -size 61961 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png index ce9c1e0af5..39c5faa542 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48c4460c9b57f0a438c9d0ae268eaa3823bddf3f1054e5c6b5caa64c6d075c6c -size 37392 +oid sha256:7e9412f3e84b63be532188e13a976d1954404466136633f3f8cd41851617e753 +size 60668 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png index c3f90f4170..df384039ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8226bbd316c5c4ac01f5660b262623be32ec0a274c696c7a547c8baf59bf069b -size 37171 +oid sha256:1da55e4b116cbf0ebd74afee2a47d34b99d87192cdf9a5f8698118fcd4bbe239 +size 45466 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png new file mode 100644 index 0000000000..fadf662965 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97d9c333fdc5684971e22593b246ca2751657691dad90ec89d891379e7a0b47f +size 60161 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png new file mode 100644 index 0000000000..068ccea0fd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c08a7e954c11ff61a9130c54c0b273a5b8996c9095a4dae07cb4b5bc357fc15f +size 58458 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png new file mode 100644 index 0000000000..82475e58db --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1967fa34ef6bd37c373f68badaed4e41b04efbbf929187e6ded4cfb903715325 +size 59539 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png new file mode 100644 index 0000000000..db2e4e318d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:430b30b65f5908fce562be613afa41131c4fbe8849db3d3f5a3fa424417469a4 +size 57827 From b387eac4487aac2d69f55e9321f702a070102774 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 21 Nov 2024 09:55:43 +0100 Subject: [PATCH 08/12] Remove default value and clarify the code --- .../element/android/libraries/textcomposer/TextComposer.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 81cc7a739f..fe6034b8b6 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -589,7 +589,8 @@ internal fun TextComposerEditCaptionPreview() = ElementPreview { state = textEditorState, voiceMessageState = VoiceMessageState.Idle, composerMode = aMessageComposerModeEditCaption( - content = "A caption", + // Set an existing caption so that the UI will be in edit caption mode + content = "An existing caption", ), enableVoiceMessages = false, ) @@ -606,6 +607,7 @@ internal fun TextComposerAddCaptionPreview() = ElementPreview { state = textEditorState, voiceMessageState = VoiceMessageState.Idle, composerMode = aMessageComposerModeEditCaption( + // No caption so that the UI will be in add caption mode content = "", ), enableVoiceMessages = false, @@ -759,7 +761,7 @@ fun aMessageComposerModeEdit( fun aMessageComposerModeEditCaption( eventOrTransactionId: EventOrTransactionId = EventId("$1234").toEventOrTransactionId(), - content: String = "Some caption", + content: String, ) = MessageComposerMode.EditCaption( eventOrTransactionId = eventOrTransactionId, content = content From cebf8a8c811a0859e45b62e2f2060e120709b592 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 21 Nov 2024 09:58:33 +0100 Subject: [PATCH 09/12] Add names to call arguments --- .../MessageComposerPresenterTest.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index c1d1fd8c11..8aa1110269 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -1427,17 +1427,17 @@ class MessageComposerPresenterTest { isRichTextEditorEnabled: Boolean = true, draftService: ComposerDraftService = FakeComposerDraftService(), ) = MessageComposerPresenter( - coroutineScope, - room, - pickerProvider, - featureFlagService, - sessionPreferencesStore, - localMediaFactory, - MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()), - snackbarDispatcher, - analyticsService, - DefaultMessageComposerContext(), - TestRichTextEditorStateFactory(), + appCoroutineScope = coroutineScope, + room = room, + mediaPickerProvider = pickerProvider, + featureFlagService = featureFlagService, + sessionPreferencesStore = sessionPreferencesStore, + localMediaFactory = localMediaFactory, + mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()), + snackbarDispatcher = snackbarDispatcher, + analyticsService = analyticsService, + messageComposerContext = DefaultMessageComposerContext(), + richTextEditorStateFactory = TestRichTextEditorStateFactory(), roomAliasSuggestionsDataSource = FakeRoomAliasSuggestionsDataSource(), permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter), permalinkParser = permalinkParser, From d643ae8cbb87b3bd54861defebb7dcf1b0557561 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 21 Nov 2024 10:19:14 +0100 Subject: [PATCH 10/12] Add missing test about sending audio. --- .../AttachmentsPreviewPresenterTest.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt index bc61728cb1..4f35c592f1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt @@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.attachments.preview.SendActionS import io.element.android.features.messages.impl.fixtures.aMediaAttachment import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.matrix.api.core.ProgressCallback +import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo @@ -41,6 +42,7 @@ import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle @@ -189,6 +191,42 @@ class AttachmentsPreviewPresenterTest { } } + @Test + fun `present - send audio with caption success scenario`() = runTest { + val sendAudioResult = + lambdaRecorder> { _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + val mediaPreProcessor = FakeMediaPreProcessor().apply { + givenAudioResult() + } + val room = FakeMatrixRoom( + sendAudioResult = sendAudioResult, + ) + val onDoneListener = lambdaRecorder { } + val presenter = createAttachmentsPreviewPresenter( + room = room, + mediaPreProcessor = mediaPreProcessor, + onDoneListener = { onDoneListener() }, + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) + initialState.textEditorState.setMarkdown(A_CAPTION) + initialState.eventSink(AttachmentsPreviewEvents.SendAttachment) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing) + advanceUntilIdle() + sendAudioResult.assertions().isCalledOnce().with( + any(), + any(), + value(A_CAPTION), + any(), + any(), + ) + onDoneListener.assertions().isCalledOnce() + } + } + @Test fun `present - send media failure scenario`() = runTest { val failure = MediaPreProcessor.Failure(null) From 6bcfa0e6c6fd939b6d064373fae4b16d364371e5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 21 Nov 2024 11:08:19 +0100 Subject: [PATCH 11/12] Add missing test on MessageComposerPresenter --- .../MessageComposerPresenterTest.kt | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index 8aa1110269..1654a061c8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_CAPTION import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_REPLY import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -87,6 +88,7 @@ import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf @@ -208,6 +210,91 @@ class MessageComposerPresenterTest { } } + @Test + fun `present - change mode to edit caption`() = runTest { + val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean -> + ComposerDraft(A_MESSAGE, A_MESSAGE, ComposerDraftType.NewMessage) + } + val updateDraftLambda = lambdaRecorder { _: RoomId, _: ComposerDraft?, _: Boolean -> } + val draftService = FakeComposerDraftService().apply { + this.loadDraftLambda = loadDraftLambda + this.saveDraftLambda = updateDraftLambda + } + val presenter = createPresenter( + coroutineScope = this, + draftService = draftService, + ) + moleculeFlow(RecompositionMode.Immediate) { + val state = presenter.present() + remember(state, state.textEditorState.messageHtml()) { state } + }.test { + var state = awaitFirstItem() + val mode = anEditCaptionMode(caption = A_CAPTION) + state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) + state = awaitItem() + assertThat(state.mode).isEqualTo(mode) + assertThat(state.textEditorState.messageHtml()).isEqualTo(A_CAPTION) + state = backToNormalMode(state) + // The caption that was being edited is cleared and volatile draft is loaded + assertThat(state.textEditorState.messageHtml()).isEqualTo(A_MESSAGE) + + assert(loadDraftLambda) + .isCalledExactly(2) + .withSequence( + // Automatic load of draft + listOf(value(A_ROOM_ID), value(false)), + // Load of volatile draft when closing edit mode + listOf(value(A_ROOM_ID), value(true)) + ) + assert(updateDraftLambda) + .isCalledOnce() + .with(value(A_ROOM_ID), any(), value(true)) + } + } + + @Test + fun `present - change mode to edit caption and send the caption`() = runTest { + val editCaptionLambda = lambdaRecorder { _: EventOrTransactionId, _: String?, _: String? -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.editCaptionLambda = editCaptionLambda + } + val fakeMatrixRoom = FakeMatrixRoom( + liveTimeline = timeline, + typingNoticeResult = { Result.success(Unit) } + ) + val presenter = createPresenter( + coroutineScope = this, + room = fakeMatrixRoom, + isRichTextEditorEnabled = false, + ) + val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("") }) + presenter.test { + var state = awaitFirstItem() + val mode = anEditCaptionMode(caption = A_CAPTION) + state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) + state = awaitItem() + assertThat(state.mode).isEqualTo(mode) + assertThat(state.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_CAPTION) + state.eventSink.invoke(MessageComposerEvents.SendMessage) + val messageSentState = awaitItem() + assertThat(messageSentState.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo("") + waitForPredicate { analyticsService.capturedEvents.size == 1 } + assertThat(analyticsService.capturedEvents).containsExactly( + Composer( + inThread = false, + isEditing = true, + isReply = false, + messageType = Composer.MessageType.Text, + ) + ) + assert(editCaptionLambda) + .isCalledOnce() + .with(value(AN_EVENT_ID.toEventOrTransactionId()), value(A_CAPTION), value(null)) + } + } + @Test fun `present - change mode to reply after edit`() = runTest { val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean -> @@ -1465,6 +1552,11 @@ fun anEditMode( message: String = A_MESSAGE, ) = MessageComposerMode.Edit(eventOrTransactionId, message) +fun anEditCaptionMode( + eventOrTransactionId: EventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(), + caption: String = A_CAPTION, +) = MessageComposerMode.EditCaption(eventOrTransactionId, caption) + fun aReplyMode() = MessageComposerMode.Reply( replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), hideImage = false, From 55032dcdc9422e99f9a75ed6ab2434eded62cacd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 21 Nov 2024 11:34:14 +0100 Subject: [PATCH 12/12] Add missing tests on MessagePresenter --- .../messages/impl/MessagesPresenterTest.kt | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index f66c8c50b8..3d4969fd28 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState @@ -59,6 +60,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_CAPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 @@ -82,6 +84,7 @@ import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -972,6 +975,103 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle action edit caption`() = runTest { + val messageEvent = aMessageEvent( + content = aTimelineItemImageContent( + caption = A_CAPTION, + ) + ) + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditCaption, messageEvent)) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.EditCaption( + eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(), + content = A_CAPTION, + ) + ) + ) + } + } + + @Test + fun `present - handle action add caption`() = runTest { + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) + val messageEvent = aMessageEvent( + content = aTimelineItemImageContent( + caption = null, + ) + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.AddCaption, messageEvent)) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.EditCaption( + eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(), + content = "", + ) + ) + ) + } + } + + @Test + fun `present - handle action remove caption`() = runTest { + val messageEvent = aMessageEvent( + content = aTimelineItemImageContent( + caption = A_CAPTION, + ) + ) + val editCaptionLambda = lambdaRecorder { _: EventOrTransactionId, _: String?, _: String? -> Result.success(Unit) } + val timeline = FakeTimeline().apply { + this.editCaptionLambda = editCaptionLambda + } + val room = FakeMatrixRoom( + liveTimeline = timeline, + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + typingNoticeResult = { Result.success(Unit) }, + canUserPinUnpinResult = { Result.success(true) }, + ) + val presenter = createMessagesPresenter( + matrixRoom = room, + ) + presenter.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.RemoveCaption, messageEvent)) + editCaptionLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID.toEventOrTransactionId()), value(null), value(null)) + } + } + + @Test + fun `present - handle action view in timeline, it should have no effect`() = runTest { + val messageEvent = aMessageEvent( + content = aTimelineItemTextContent() + ) + val presenter = createMessagesPresenter() + presenter.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewInTimeline, messageEvent)) + // No op! + } + } + private fun TestScope.createMessagesPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixRoom: MatrixRoom = FakeMatrixRoom(