Send caption with media

This commit is contained in:
Benoit Marty 2024-11-04 12:54:11 +01:00 committed by Benoit Marty
parent 17ea2aa5dc
commit 223eae9602
19 changed files with 301 additions and 76 deletions

View file

@ -9,16 +9,21 @@ package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.textcomposer.model.TextEditorState
import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@ -30,6 +35,7 @@ import kotlin.coroutines.coroutineContext
class AttachmentsPreviewPresenter @AssistedInject constructor(
@Assisted private val attachment: Attachment,
private val mediaSender: MediaSender,
private val permalinkBuilder: PermalinkBuilder,
) : Presenter<AttachmentsPreviewState> {
@AssistedFactory
interface Factory {
@ -44,11 +50,24 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mutableStateOf<SendActionState>(SendActionState.Idle)
}
val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false)
val textEditorState by rememberUpdatedState(
TextEditorState.Markdown(markdownTextEditorState)
)
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
when (attachmentsPreviewEvents) {
AttachmentsPreviewEvents.SendAttachment -> ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(attachment, sendActionState)
is AttachmentsPreviewEvents.SendAttachment -> {
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
.takeIf { it.isNotEmpty() }
ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(
attachment = attachment,
caption = caption,
sendActionState = sendActionState,
)
}
AttachmentsPreviewEvents.ClearSendState -> {
ongoingSendAttachmentJob.value?.let {
it.cancel()
@ -62,18 +81,21 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
return AttachmentsPreviewState(
attachment = attachment,
sendActionState = sendActionState.value,
textEditorState = textEditorState,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.sendAttachment(
attachment: Attachment,
caption: String?,
sendActionState: MutableState<SendActionState>,
) = launch {
when (attachment) {
is Attachment.Media -> {
sendMedia(
mediaAttachment = attachment,
caption = caption,
sendActionState = sendActionState,
)
}
@ -82,6 +104,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
private suspend fun sendMedia(
mediaAttachment: Attachment.Media,
caption: String?,
sendActionState: MutableState<SendActionState>,
) = runCatching {
val context = coroutineContext
@ -96,6 +119,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mediaSender.sendMedia(
uri = mediaAttachment.localMedia.uri,
mimeType = mediaAttachment.localMedia.info.mimeType,
caption = caption,
progressCallback = progressCallback
).getOrThrow()
}.fold(

View file

@ -9,10 +9,12 @@ 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.textcomposer.model.TextEditorState
data class AttachmentsPreviewState(
val attachment: Attachment,
val sendActionState: SendActionState,
val textEditorState: TextEditorState,
val eventSink: (AttachmentsPreviewEvents) -> Unit
)

View file

@ -14,6 +14,8 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
import io.element.android.libraries.textcomposer.model.TextEditorState
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
override val values: Sequence<AttachmentsPreviewState>
@ -27,11 +29,13 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
fun anAttachmentsPreviewState(
mediaInfo: MediaInfo = anImageMediaInfo(),
sendActionState: SendActionState = SendActionState.Idle
textEditorState: TextEditorState = aTextEditorStateMarkdown(),
sendActionState: SendActionState = SendActionState.Idle,
) = AttachmentsPreviewState(
attachment = Attachment.Media(
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
),
sendActionState = sendActionState,
textEditorState = textEditorState,
eventSink = {}
)

View file

@ -9,37 +9,44 @@ package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.ProgressDialogType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
import io.element.android.libraries.textcomposer.TextComposer
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.VoiceMessageState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.wysiwyg.display.TextDisplay
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.rememberZoomableState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AttachmentsPreviewView(
state: AttachmentsPreviewState,
@ -61,11 +68,23 @@ fun AttachmentsPreviewView(
}
}
Scaffold(modifier) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = {
BackButton(
imageVector = CompoundIcons.Close(),
onClick = onDismiss,
)
},
title = {},
)
}
) {
AttachmentPreviewContent(
attachment = state.attachment,
state = state,
onSendClick = ::postSendAttachment,
onDismiss = onDismiss
)
}
AttachmentSendStateView(
@ -106,21 +125,19 @@ private fun AttachmentSendStateView(
@Composable
private fun AttachmentPreviewContent(
attachment: Attachment,
state: AttachmentsPreviewState,
onSendClick: () -> Unit,
onDismiss: () -> Unit,
) {
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding(),
contentAlignment = Alignment.BottomCenter
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
when (attachment) {
when (val attachment = state.attachment) {
is Attachment.Media -> {
val localMediaViewState = rememberLocalMediaViewState(
zoomableState = rememberZoomableState(
@ -137,27 +154,46 @@ private fun AttachmentPreviewContent(
}
}
AttachmentsPreviewBottomActions(
onCancelClick = onDismiss,
state = state,
onSendClick = onSendClick,
modifier = Modifier
.fillMaxWidth()
.background(Color.Black.copy(alpha = 0.7f))
.padding(horizontal = 24.dp)
.defaultMinSize(minHeight = 80.dp)
.background(ElementTheme.colors.bgCanvasDefault)
.height(IntrinsicSize.Min)
.align(Alignment.BottomCenter)
.imePadding(),
)
}
}
@Composable
private fun AttachmentsPreviewBottomActions(
onCancelClick: () -> Unit,
state: AttachmentsPreviewState,
onSendClick: () -> Unit,
modifier: Modifier = Modifier
) {
ButtonRowMolecule(modifier = modifier) {
TextButton(stringResource(id = CommonStrings.action_cancel), onClick = onCancelClick)
TextButton(stringResource(id = CommonStrings.action_send), onClick = onSendClick)
}
TextComposer(
modifier = modifier,
state = state.textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Caption,
onRequestFocus = {},
onSendMessage = onSendClick,
showTextFormatting = false,
onResetComposerMode = {},
onAddAttachment = {},
onDismissTextFormatting = {},
enableVoiceMessages = false,
onVoiceRecorderEvent = {},
onVoicePlayerEvent = {},
onSendVoiceMessage = {},
onDeleteVoiceMessage = {},
onReceiveSuggestion = {},
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
onError = {},
onTyping = {},
onSelectRichContent = {},
)
}
// Only preview in dark, dark theme is forced on the Node.

View file

@ -436,6 +436,7 @@ class MessageComposerPresenter @Inject constructor(
// Reset composer right away
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
when (capturedMode) {
is MessageComposerMode.Caption,
is MessageComposerMode.Normal -> room.sendMessage(
body = message.markdown,
htmlBody = message.html,
@ -605,6 +606,7 @@ class MessageComposerPresenter @Inject constructor(
): ComposerDraft? {
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = false)
val draftType = when (val mode = messageComposerContext.composerMode) {
is MessageComposerMode.Caption,
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
is MessageComposerMode.Edit -> {
mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }

View file

@ -20,8 +20,13 @@ import io.element.android.features.messages.impl.attachments.preview.SendActionS
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
import io.element.android.libraries.matrix.api.core.ProgressCallback
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
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.A_CAPTION
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.room.FakeMatrixRoom
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
@ -30,19 +35,23 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
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.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.File
@RunWith(RobolectricTestRunner::class)
class AttachmentsPreviewPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
private val mediaPreProcessor = FakeMediaPreProcessor()
private val mockMediaUrl: Uri = mockk("localMediaUri")
@Test
@ -75,6 +84,80 @@ class AttachmentsPreviewPresenterTest {
}
}
@Test
fun `present - send image with caption success scenario`() = runTest {
val sendImageResult =
lambdaRecorder<File, File?, ImageInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val mediaPreProcessor = FakeMediaPreProcessor().apply {
givenImageResult()
}
val room = FakeMatrixRoom(
sendImageResult = sendImageResult,
)
val presenter = createAttachmentsPreviewPresenter(
room = room,
mediaPreProcessor = mediaPreProcessor,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.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)
val successState = awaitItem()
assertThat(successState.sendActionState).isEqualTo(SendActionState.Done)
sendImageResult.assertions().isCalledOnce().with(
any(),
any(),
any(),
value(A_CAPTION),
any(),
any(),
)
}
}
@Test
fun `present - send video with caption success scenario`() = runTest {
val sendVideoResult =
lambdaRecorder<File, File?, VideoInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val mediaPreProcessor = FakeMediaPreProcessor().apply {
givenVideoResult()
}
val room = FakeMatrixRoom(
sendVideoResult = sendVideoResult,
)
val presenter = createAttachmentsPreviewPresenter(
room = room,
mediaPreProcessor = mediaPreProcessor,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.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)
val successState = awaitItem()
assertThat(successState.sendActionState).isEqualTo(SendActionState.Done)
sendVideoResult.assertions().isCalledOnce().with(
any(),
any(),
any(),
value(A_CAPTION),
any(),
any(),
)
}
}
@Test
fun `present - send media failure scenario`() = runTest {
val failure = MediaPreProcessor.Failure(null)
@ -121,11 +204,14 @@ class AttachmentsPreviewPresenterTest {
localMedia: LocalMedia = aLocalMedia(
uri = mockMediaUrl,
),
room: MatrixRoom = FakeMatrixRoom()
room: MatrixRoom = FakeMatrixRoom(),
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
): AttachmentsPreviewPresenter {
return AttachmentsPreviewPresenter(
attachment = aMediaAttachment(localMedia),
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore())
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
permalinkBuilder = permalinkBuilder,
)
}
}

View file

@ -132,8 +132,8 @@ interface MatrixRoom : Closeable {
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler>
@ -141,8 +141,8 @@ interface MatrixRoom : Closeable {
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler>

View file

@ -75,8 +75,8 @@ interface Timeline : AutoCloseable {
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler>
@ -84,8 +84,8 @@ interface Timeline : AutoCloseable {
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler>

View file

@ -445,22 +445,22 @@ class RustMatrixRoom(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> {
return liveTimeline.sendImage(file, thumbnailFile, imageInfo, body, formattedBody, progressCallback)
return liveTimeline.sendImage(file, thumbnailFile, imageInfo, caption, formattedCaption, progressCallback)
}
override suspend fun sendVideo(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> {
return liveTimeline.sendVideo(file, thumbnailFile, videoInfo, body, formattedBody, progressCallback)
return liveTimeline.sendVideo(file, thumbnailFile, videoInfo, caption, formattedCaption, progressCallback)
}
override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> {

View file

@ -326,8 +326,8 @@ class RustTimeline(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> {
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
@ -335,8 +335,8 @@ class RustTimeline(
url = file.path,
thumbnailUrl = thumbnailFile?.path,
imageInfo = imageInfo.map(),
caption = body,
formattedCaption = formattedBody?.let {
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
storeInCache = true,
@ -349,8 +349,8 @@ class RustTimeline(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> {
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
@ -358,8 +358,8 @@ class RustTimeline(
url = file.path,
thumbnailUrl = thumbnailFile?.path,
videoInfo = videoInfo.map(),
caption = body,
formattedCaption = formattedBody?.let {
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
storeInCache = true,

View file

@ -61,6 +61,7 @@ const val A_ROOM_RAW_NAME = "A room raw name"
const val A_MESSAGE = "Hello world!"
const val A_REPLY = "OK, I'll be there!"
const val ANOTHER_MESSAGE = "Hello universe!"
const val A_CAPTION = "A media caption"
const val A_REDACTION_REASON = "A redaction reason"

View file

@ -321,8 +321,8 @@ class FakeMatrixRoom(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
@ -330,8 +330,8 @@ class FakeMatrixRoom(
file,
thumbnailFile,
imageInfo,
body,
formattedBody,
caption,
formattedCaption,
progressCallback,
)
}
@ -340,8 +340,8 @@ class FakeMatrixRoom(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
@ -349,8 +349,8 @@ class FakeMatrixRoom(
file,
thumbnailFile,
videoInfo,
body,
formattedBody,
caption,
formattedCaption,
progressCallback,
)
}

View file

@ -131,15 +131,15 @@ class FakeTimeline(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> = sendImageLambda(
file,
thumbnailFile,
imageInfo,
body,
formattedBody,
caption,
formattedCaption,
progressCallback
)
@ -158,15 +158,15 @@ class FakeTimeline(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
body: String?,
formattedBody: String?,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> = sendVideoLambda(
file,
thumbnailFile,
videoInfo,
body,
formattedBody,
caption,
formattedCaption,
progressCallback
)

View file

@ -106,8 +106,8 @@ class MediaSender @Inject constructor(
file = uploadInfo.file,
thumbnailFile = uploadInfo.thumbnailFile,
imageInfo = uploadInfo.imageInfo,
body = caption,
formattedBody = formattedCaption,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback
)
}
@ -116,8 +116,8 @@ class MediaSender @Inject constructor(
file = uploadInfo.file,
thumbnailFile = uploadInfo.thumbnailFile,
videoInfo = uploadInfo.videoInfo,
body = caption,
formattedBody = formattedCaption,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback
)
}

View file

@ -11,6 +11,8 @@ import android.net.Uri
import io.element.android.libraries.core.mimetype.MimeTypes
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
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.tests.testutils.simulateLongTask
@ -61,4 +63,45 @@ class FakeMediaPreProcessor : MediaPreProcessor {
)
)
}
fun givenImageResult() {
givenResult(
Result.success(
MediaUploadInfo.Image(
file = File("image.jpg"),
imageInfo = ImageInfo(
height = 100,
width = 100,
mimetype = MimeTypes.Jpeg,
size = 1000,
thumbnailInfo = null,
thumbnailSource = null,
blurhash = null,
),
thumbnailFile = null,
)
)
)
}
fun givenVideoResult() {
givenResult(
Result.success(
MediaUploadInfo.Video(
file = File("image.jpg"),
videoInfo = VideoInfo(
duration = 1000.seconds,
height = 100,
width = 100,
mimetype = MimeTypes.Mp4,
size = 1000,
thumbnailInfo = null,
thumbnailSource = null,
blurhash = null,
),
thumbnailFile = null,
)
)
)
}
}

View file

@ -125,16 +125,22 @@ fun TextComposer(
val composerOptionsButton: @Composable () -> Unit = remember {
@Composable {
ComposerOptionsButton(
modifier = Modifier
.size(48.dp),
onClick = onAddAttachment
)
if (composerMode == MessageComposerMode.Caption) {
Spacer(modifier = Modifier.width(9.dp))
} else {
ComposerOptionsButton(
modifier = Modifier
.size(48.dp),
onClick = onAddAttachment
)
}
}
}
val placeholder = if (composerMode.inThread) {
stringResource(id = CommonStrings.action_reply_in_thread)
} else if (composerMode == MessageComposerMode.Caption) {
stringResource(id = R.string.rich_text_editor_composer_caption_placeholder)
} else {
stringResource(id = R.string.rich_text_editor_composer_placeholder)
}
@ -180,7 +186,7 @@ fun TextComposer(
}
}
val canSendMessage = markdown.isNotBlank()
val canSendMessage = markdown.isNotBlank() || composerMode == MessageComposerMode.Caption
val sendButton = @Composable {
SendButton(
canSendMessage = canSendMessage,
@ -592,6 +598,21 @@ internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerCaptionPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview {
PreviewColumn(
items = aTextEditorStateMarkdownList()
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Caption,
enableVoiceMessages = false,
)
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerVoicePreview() = ElementPreview {

View file

@ -18,6 +18,8 @@ import io.element.android.libraries.matrix.ui.messages.reply.eventId
sealed interface MessageComposerMode {
data object Normal : MessageComposerMode
data object Caption : MessageComposerMode
sealed interface Special : MessageComposerMode
data class Edit(
@ -34,7 +36,8 @@ sealed interface MessageComposerMode {
val relatedEventId: EventId?
get() = when (this) {
is Normal -> null
is Normal,
is Caption -> null
is Edit -> eventOrTransactionId.eventId
is Reply -> eventId
}

View file

@ -36,6 +36,7 @@ sealed interface TextEditorState {
is Rich -> richTextEditorState.hasFocus
}
// Note: for test only
suspend fun setHtml(html: String) {
when (this) {
is Markdown -> Unit
@ -43,6 +44,7 @@ sealed interface TextEditorState {
}
}
// Note: for test only
suspend fun setMarkdown(text: String) {
when (this) {
is Markdown -> state.text.update(text, true)

View file

@ -101,6 +101,7 @@ class KonsistPreviewTest {
"SasEmojisPreview",
"SecureBackupSetupViewChangePreview",
"SelectedUserCannotRemovePreview",
"TextComposerCaptionPreview",
"TextComposerEditPreview",
"TextComposerFormattingPreview",
"TextComposerLinkDialogCreateLinkPreview",