Send caption with media
This commit is contained in:
parent
0da504542f
commit
19c56cdbe3
19 changed files with 301 additions and 76 deletions
|
|
@ -9,16 +9,21 @@ package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
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.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.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -30,6 +35,7 @@ import kotlin.coroutines.coroutineContext
|
||||||
class AttachmentsPreviewPresenter @AssistedInject constructor(
|
class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
@Assisted private val attachment: Attachment,
|
@Assisted private val attachment: Attachment,
|
||||||
private val mediaSender: MediaSender,
|
private val mediaSender: MediaSender,
|
||||||
|
private val permalinkBuilder: PermalinkBuilder,
|
||||||
) : Presenter<AttachmentsPreviewState> {
|
) : Presenter<AttachmentsPreviewState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -44,11 +50,24 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
mutableStateOf<SendActionState>(SendActionState.Idle)
|
mutableStateOf<SendActionState>(SendActionState.Idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false)
|
||||||
|
val textEditorState by rememberUpdatedState(
|
||||||
|
TextEditorState.Markdown(markdownTextEditorState)
|
||||||
|
)
|
||||||
|
|
||||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
||||||
when (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 -> {
|
AttachmentsPreviewEvents.ClearSendState -> {
|
||||||
ongoingSendAttachmentJob.value?.let {
|
ongoingSendAttachmentJob.value?.let {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
|
|
@ -62,18 +81,21 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
return AttachmentsPreviewState(
|
return AttachmentsPreviewState(
|
||||||
attachment = attachment,
|
attachment = attachment,
|
||||||
sendActionState = sendActionState.value,
|
sendActionState = sendActionState.value,
|
||||||
|
textEditorState = textEditorState,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.sendAttachment(
|
private fun CoroutineScope.sendAttachment(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
|
caption: String?,
|
||||||
sendActionState: MutableState<SendActionState>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = launch {
|
) = launch {
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
sendMedia(
|
sendMedia(
|
||||||
mediaAttachment = attachment,
|
mediaAttachment = attachment,
|
||||||
|
caption = caption,
|
||||||
sendActionState = sendActionState,
|
sendActionState = sendActionState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +104,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private suspend fun sendMedia(
|
private suspend fun sendMedia(
|
||||||
mediaAttachment: Attachment.Media,
|
mediaAttachment: Attachment.Media,
|
||||||
|
caption: String?,
|
||||||
sendActionState: MutableState<SendActionState>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = runCatching {
|
) = runCatching {
|
||||||
val context = coroutineContext
|
val context = coroutineContext
|
||||||
|
|
@ -96,6 +119,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
mediaSender.sendMedia(
|
mediaSender.sendMedia(
|
||||||
uri = mediaAttachment.localMedia.uri,
|
uri = mediaAttachment.localMedia.uri,
|
||||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||||
|
caption = caption,
|
||||||
progressCallback = progressCallback
|
progressCallback = progressCallback
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
}.fold(
|
}.fold(
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
|
|
||||||
data class AttachmentsPreviewState(
|
data class AttachmentsPreviewState(
|
||||||
val attachment: Attachment,
|
val attachment: Attachment,
|
||||||
val sendActionState: SendActionState,
|
val sendActionState: SendActionState,
|
||||||
|
val textEditorState: TextEditorState,
|
||||||
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
|
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
|
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> {
|
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
|
||||||
override val values: Sequence<AttachmentsPreviewState>
|
override val values: Sequence<AttachmentsPreviewState>
|
||||||
|
|
@ -27,11 +29,13 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
|
||||||
|
|
||||||
fun anAttachmentsPreviewState(
|
fun anAttachmentsPreviewState(
|
||||||
mediaInfo: MediaInfo = anImageMediaInfo(),
|
mediaInfo: MediaInfo = anImageMediaInfo(),
|
||||||
sendActionState: SendActionState = SendActionState.Idle
|
textEditorState: TextEditorState = aTextEditorStateMarkdown(),
|
||||||
|
sendActionState: SendActionState = SendActionState.Idle,
|
||||||
) = AttachmentsPreviewState(
|
) = AttachmentsPreviewState(
|
||||||
attachment = Attachment.Media(
|
attachment = Attachment.Media(
|
||||||
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
||||||
),
|
),
|
||||||
sendActionState = sendActionState,
|
sendActionState = sendActionState,
|
||||||
|
textEditorState = textEditorState,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,37 +9,44 @@ package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
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.Attachment
|
||||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
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.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialogType
|
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.components.dialogs.RetryDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
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.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.LocalMediaView
|
||||||
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
|
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.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.wysiwyg.display.TextDisplay
|
||||||
import me.saket.telephoto.zoomable.ZoomSpec
|
import me.saket.telephoto.zoomable.ZoomSpec
|
||||||
import me.saket.telephoto.zoomable.rememberZoomableState
|
import me.saket.telephoto.zoomable.rememberZoomableState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AttachmentsPreviewView(
|
fun AttachmentsPreviewView(
|
||||||
state: AttachmentsPreviewState,
|
state: AttachmentsPreviewState,
|
||||||
|
|
@ -61,11 +68,23 @@ fun AttachmentsPreviewView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(modifier) {
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(
|
||||||
|
imageVector = CompoundIcons.Close(),
|
||||||
|
onClick = onDismiss,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
AttachmentPreviewContent(
|
AttachmentPreviewContent(
|
||||||
attachment = state.attachment,
|
state = state,
|
||||||
onSendClick = ::postSendAttachment,
|
onSendClick = ::postSendAttachment,
|
||||||
onDismiss = onDismiss
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AttachmentSendStateView(
|
AttachmentSendStateView(
|
||||||
|
|
@ -106,21 +125,19 @@ private fun AttachmentSendStateView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AttachmentPreviewContent(
|
private fun AttachmentPreviewContent(
|
||||||
attachment: Attachment,
|
state: AttachmentsPreviewState,
|
||||||
onSendClick: () -> Unit,
|
onSendClick: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding(),
|
.navigationBarsPadding(),
|
||||||
contentAlignment = Alignment.BottomCenter
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
when (attachment) {
|
when (val attachment = state.attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
val localMediaViewState = rememberLocalMediaViewState(
|
val localMediaViewState = rememberLocalMediaViewState(
|
||||||
zoomableState = rememberZoomableState(
|
zoomableState = rememberZoomableState(
|
||||||
|
|
@ -137,27 +154,46 @@ private fun AttachmentPreviewContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AttachmentsPreviewBottomActions(
|
AttachmentsPreviewBottomActions(
|
||||||
onCancelClick = onDismiss,
|
state = state,
|
||||||
onSendClick = onSendClick,
|
onSendClick = onSendClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color.Black.copy(alpha = 0.7f))
|
.background(ElementTheme.colors.bgCanvasDefault)
|
||||||
.padding(horizontal = 24.dp)
|
.height(IntrinsicSize.Min)
|
||||||
.defaultMinSize(minHeight = 80.dp)
|
.align(Alignment.BottomCenter)
|
||||||
|
.imePadding(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AttachmentsPreviewBottomActions(
|
private fun AttachmentsPreviewBottomActions(
|
||||||
onCancelClick: () -> Unit,
|
state: AttachmentsPreviewState,
|
||||||
onSendClick: () -> Unit,
|
onSendClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
ButtonRowMolecule(modifier = modifier) {
|
TextComposer(
|
||||||
TextButton(stringResource(id = CommonStrings.action_cancel), onClick = onCancelClick)
|
modifier = modifier,
|
||||||
TextButton(stringResource(id = CommonStrings.action_send), onClick = onSendClick)
|
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.
|
// Only preview in dark, dark theme is forced on the Node.
|
||||||
|
|
|
||||||
|
|
@ -436,6 +436,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
// Reset composer right away
|
// Reset composer right away
|
||||||
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
|
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
|
||||||
when (capturedMode) {
|
when (capturedMode) {
|
||||||
|
is MessageComposerMode.Caption,
|
||||||
is MessageComposerMode.Normal -> room.sendMessage(
|
is MessageComposerMode.Normal -> room.sendMessage(
|
||||||
body = message.markdown,
|
body = message.markdown,
|
||||||
htmlBody = message.html,
|
htmlBody = message.html,
|
||||||
|
|
@ -605,6 +606,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
): ComposerDraft? {
|
): ComposerDraft? {
|
||||||
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = false)
|
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = false)
|
||||||
val draftType = when (val mode = messageComposerContext.composerMode) {
|
val draftType = when (val mode = messageComposerContext.composerMode) {
|
||||||
|
is MessageComposerMode.Caption,
|
||||||
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
|
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
||||||
|
|
|
||||||
|
|
@ -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.features.messages.impl.fixtures.aMediaAttachment
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
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.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.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.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.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
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.mediaviewer.test.viewer.aLocalMedia
|
||||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
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.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class AttachmentsPreviewPresenterTest {
|
class AttachmentsPreviewPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
private val mediaPreProcessor = FakeMediaPreProcessor()
|
|
||||||
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
||||||
|
|
||||||
@Test
|
@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
|
@Test
|
||||||
fun `present - send media failure scenario`() = runTest {
|
fun `present - send media failure scenario`() = runTest {
|
||||||
val failure = MediaPreProcessor.Failure(null)
|
val failure = MediaPreProcessor.Failure(null)
|
||||||
|
|
@ -121,11 +204,14 @@ class AttachmentsPreviewPresenterTest {
|
||||||
localMedia: LocalMedia = aLocalMedia(
|
localMedia: LocalMedia = aLocalMedia(
|
||||||
uri = mockMediaUrl,
|
uri = mockMediaUrl,
|
||||||
),
|
),
|
||||||
room: MatrixRoom = FakeMatrixRoom()
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
|
||||||
|
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
): AttachmentsPreviewPresenter {
|
): AttachmentsPreviewPresenter {
|
||||||
return AttachmentsPreviewPresenter(
|
return AttachmentsPreviewPresenter(
|
||||||
attachment = aMediaAttachment(localMedia),
|
attachment = aMediaAttachment(localMedia),
|
||||||
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore())
|
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
|
||||||
|
permalinkBuilder = permalinkBuilder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,8 @@ interface MatrixRoom : Closeable {
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
imageInfo: ImageInfo,
|
imageInfo: ImageInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<MediaUploadHandler>
|
): Result<MediaUploadHandler>
|
||||||
|
|
||||||
|
|
@ -141,8 +141,8 @@ interface MatrixRoom : Closeable {
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<MediaUploadHandler>
|
): Result<MediaUploadHandler>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,8 @@ interface Timeline : AutoCloseable {
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
imageInfo: ImageInfo,
|
imageInfo: ImageInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<MediaUploadHandler>
|
): Result<MediaUploadHandler>
|
||||||
|
|
||||||
|
|
@ -84,8 +84,8 @@ interface Timeline : AutoCloseable {
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<MediaUploadHandler>
|
): Result<MediaUploadHandler>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -445,22 +445,22 @@ class RustMatrixRoom(
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
imageInfo: ImageInfo,
|
imageInfo: ImageInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?,
|
progressCallback: ProgressCallback?,
|
||||||
): Result<MediaUploadHandler> {
|
): Result<MediaUploadHandler> {
|
||||||
return liveTimeline.sendImage(file, thumbnailFile, imageInfo, body, formattedBody, progressCallback)
|
return liveTimeline.sendImage(file, thumbnailFile, imageInfo, caption, formattedCaption, progressCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendVideo(
|
override suspend fun sendVideo(
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?,
|
progressCallback: ProgressCallback?,
|
||||||
): Result<MediaUploadHandler> {
|
): 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> {
|
override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> {
|
||||||
|
|
|
||||||
|
|
@ -326,8 +326,8 @@ class RustTimeline(
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
imageInfo: ImageInfo,
|
imageInfo: ImageInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?,
|
progressCallback: ProgressCallback?,
|
||||||
): Result<MediaUploadHandler> {
|
): Result<MediaUploadHandler> {
|
||||||
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
|
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
|
||||||
|
|
@ -335,8 +335,8 @@ class RustTimeline(
|
||||||
url = file.path,
|
url = file.path,
|
||||||
thumbnailUrl = thumbnailFile?.path,
|
thumbnailUrl = thumbnailFile?.path,
|
||||||
imageInfo = imageInfo.map(),
|
imageInfo = imageInfo.map(),
|
||||||
caption = body,
|
caption = caption,
|
||||||
formattedCaption = formattedBody?.let {
|
formattedCaption = formattedCaption?.let {
|
||||||
FormattedBody(body = it, format = MessageFormat.Html)
|
FormattedBody(body = it, format = MessageFormat.Html)
|
||||||
},
|
},
|
||||||
storeInCache = true,
|
storeInCache = true,
|
||||||
|
|
@ -349,8 +349,8 @@ class RustTimeline(
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?,
|
progressCallback: ProgressCallback?,
|
||||||
): Result<MediaUploadHandler> {
|
): Result<MediaUploadHandler> {
|
||||||
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
|
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
|
||||||
|
|
@ -358,8 +358,8 @@ class RustTimeline(
|
||||||
url = file.path,
|
url = file.path,
|
||||||
thumbnailUrl = thumbnailFile?.path,
|
thumbnailUrl = thumbnailFile?.path,
|
||||||
videoInfo = videoInfo.map(),
|
videoInfo = videoInfo.map(),
|
||||||
caption = body,
|
caption = caption,
|
||||||
formattedCaption = formattedBody?.let {
|
formattedCaption = formattedCaption?.let {
|
||||||
FormattedBody(body = it, format = MessageFormat.Html)
|
FormattedBody(body = it, format = MessageFormat.Html)
|
||||||
},
|
},
|
||||||
storeInCache = true,
|
storeInCache = true,
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ const val A_ROOM_RAW_NAME = "A room raw name"
|
||||||
const val A_MESSAGE = "Hello world!"
|
const val A_MESSAGE = "Hello world!"
|
||||||
const val A_REPLY = "OK, I'll be there!"
|
const val A_REPLY = "OK, I'll be there!"
|
||||||
const val ANOTHER_MESSAGE = "Hello universe!"
|
const val ANOTHER_MESSAGE = "Hello universe!"
|
||||||
|
const val A_CAPTION = "A media caption"
|
||||||
|
|
||||||
const val A_REDACTION_REASON = "A redaction reason"
|
const val A_REDACTION_REASON = "A redaction reason"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -321,8 +321,8 @@ class FakeMatrixRoom(
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
imageInfo: ImageInfo,
|
imageInfo: ImageInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<MediaUploadHandler> = simulateLongTask {
|
): Result<MediaUploadHandler> = simulateLongTask {
|
||||||
simulateSendMediaProgress(progressCallback)
|
simulateSendMediaProgress(progressCallback)
|
||||||
|
|
@ -330,8 +330,8 @@ class FakeMatrixRoom(
|
||||||
file,
|
file,
|
||||||
thumbnailFile,
|
thumbnailFile,
|
||||||
imageInfo,
|
imageInfo,
|
||||||
body,
|
caption,
|
||||||
formattedBody,
|
formattedCaption,
|
||||||
progressCallback,
|
progressCallback,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -340,8 +340,8 @@ class FakeMatrixRoom(
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<MediaUploadHandler> = simulateLongTask {
|
): Result<MediaUploadHandler> = simulateLongTask {
|
||||||
simulateSendMediaProgress(progressCallback)
|
simulateSendMediaProgress(progressCallback)
|
||||||
|
|
@ -349,8 +349,8 @@ class FakeMatrixRoom(
|
||||||
file,
|
file,
|
||||||
thumbnailFile,
|
thumbnailFile,
|
||||||
videoInfo,
|
videoInfo,
|
||||||
body,
|
caption,
|
||||||
formattedBody,
|
formattedCaption,
|
||||||
progressCallback,
|
progressCallback,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,15 +131,15 @@ class FakeTimeline(
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
imageInfo: ImageInfo,
|
imageInfo: ImageInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?,
|
progressCallback: ProgressCallback?,
|
||||||
): Result<MediaUploadHandler> = sendImageLambda(
|
): Result<MediaUploadHandler> = sendImageLambda(
|
||||||
file,
|
file,
|
||||||
thumbnailFile,
|
thumbnailFile,
|
||||||
imageInfo,
|
imageInfo,
|
||||||
body,
|
caption,
|
||||||
formattedBody,
|
formattedCaption,
|
||||||
progressCallback
|
progressCallback
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -158,15 +158,15 @@ class FakeTimeline(
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
body: String?,
|
caption: String?,
|
||||||
formattedBody: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?,
|
progressCallback: ProgressCallback?,
|
||||||
): Result<MediaUploadHandler> = sendVideoLambda(
|
): Result<MediaUploadHandler> = sendVideoLambda(
|
||||||
file,
|
file,
|
||||||
thumbnailFile,
|
thumbnailFile,
|
||||||
videoInfo,
|
videoInfo,
|
||||||
body,
|
caption,
|
||||||
formattedBody,
|
formattedCaption,
|
||||||
progressCallback
|
progressCallback
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,8 @@ class MediaSender @Inject constructor(
|
||||||
file = uploadInfo.file,
|
file = uploadInfo.file,
|
||||||
thumbnailFile = uploadInfo.thumbnailFile,
|
thumbnailFile = uploadInfo.thumbnailFile,
|
||||||
imageInfo = uploadInfo.imageInfo,
|
imageInfo = uploadInfo.imageInfo,
|
||||||
body = caption,
|
caption = caption,
|
||||||
formattedBody = formattedCaption,
|
formattedCaption = formattedCaption,
|
||||||
progressCallback = progressCallback
|
progressCallback = progressCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -116,8 +116,8 @@ class MediaSender @Inject constructor(
|
||||||
file = uploadInfo.file,
|
file = uploadInfo.file,
|
||||||
thumbnailFile = uploadInfo.thumbnailFile,
|
thumbnailFile = uploadInfo.thumbnailFile,
|
||||||
videoInfo = uploadInfo.videoInfo,
|
videoInfo = uploadInfo.videoInfo,
|
||||||
body = caption,
|
caption = caption,
|
||||||
formattedBody = formattedCaption,
|
formattedCaption = formattedCaption,
|
||||||
progressCallback = progressCallback
|
progressCallback = progressCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import android.net.Uri
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
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.AudioInfo
|
||||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
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.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,16 +125,22 @@ fun TextComposer(
|
||||||
|
|
||||||
val composerOptionsButton: @Composable () -> Unit = remember {
|
val composerOptionsButton: @Composable () -> Unit = remember {
|
||||||
@Composable {
|
@Composable {
|
||||||
ComposerOptionsButton(
|
if (composerMode == MessageComposerMode.Caption) {
|
||||||
modifier = Modifier
|
Spacer(modifier = Modifier.width(9.dp))
|
||||||
.size(48.dp),
|
} else {
|
||||||
onClick = onAddAttachment
|
ComposerOptionsButton(
|
||||||
)
|
modifier = Modifier
|
||||||
|
.size(48.dp),
|
||||||
|
onClick = onAddAttachment
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val placeholder = if (composerMode.inThread) {
|
val placeholder = if (composerMode.inThread) {
|
||||||
stringResource(id = CommonStrings.action_reply_in_thread)
|
stringResource(id = CommonStrings.action_reply_in_thread)
|
||||||
|
} else if (composerMode == MessageComposerMode.Caption) {
|
||||||
|
stringResource(id = R.string.rich_text_editor_composer_caption_placeholder)
|
||||||
} else {
|
} else {
|
||||||
stringResource(id = R.string.rich_text_editor_composer_placeholder)
|
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 {
|
val sendButton = @Composable {
|
||||||
SendButton(
|
SendButton(
|
||||||
canSendMessage = canSendMessage,
|
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
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextComposerVoicePreview() = ElementPreview {
|
internal fun TextComposerVoicePreview() = ElementPreview {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import io.element.android.libraries.matrix.ui.messages.reply.eventId
|
||||||
sealed interface MessageComposerMode {
|
sealed interface MessageComposerMode {
|
||||||
data object Normal : MessageComposerMode
|
data object Normal : MessageComposerMode
|
||||||
|
|
||||||
|
data object Caption : MessageComposerMode
|
||||||
|
|
||||||
sealed interface Special : MessageComposerMode
|
sealed interface Special : MessageComposerMode
|
||||||
|
|
||||||
data class Edit(
|
data class Edit(
|
||||||
|
|
@ -34,7 +36,8 @@ sealed interface MessageComposerMode {
|
||||||
|
|
||||||
val relatedEventId: EventId?
|
val relatedEventId: EventId?
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
is Normal -> null
|
is Normal,
|
||||||
|
is Caption -> null
|
||||||
is Edit -> eventOrTransactionId.eventId
|
is Edit -> eventOrTransactionId.eventId
|
||||||
is Reply -> eventId
|
is Reply -> eventId
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ sealed interface TextEditorState {
|
||||||
is Rich -> richTextEditorState.hasFocus
|
is Rich -> richTextEditorState.hasFocus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: for test only
|
||||||
suspend fun setHtml(html: String) {
|
suspend fun setHtml(html: String) {
|
||||||
when (this) {
|
when (this) {
|
||||||
is Markdown -> Unit
|
is Markdown -> Unit
|
||||||
|
|
@ -43,6 +44,7 @@ sealed interface TextEditorState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: for test only
|
||||||
suspend fun setMarkdown(text: String) {
|
suspend fun setMarkdown(text: String) {
|
||||||
when (this) {
|
when (this) {
|
||||||
is Markdown -> state.text.update(text, true)
|
is Markdown -> state.text.update(text, true)
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ class KonsistPreviewTest {
|
||||||
"SasEmojisPreview",
|
"SasEmojisPreview",
|
||||||
"SecureBackupSetupViewChangePreview",
|
"SecureBackupSetupViewChangePreview",
|
||||||
"SelectedUserCannotRemovePreview",
|
"SelectedUserCannotRemovePreview",
|
||||||
|
"TextComposerCaptionPreview",
|
||||||
"TextComposerEditPreview",
|
"TextComposerEditPreview",
|
||||||
"TextComposerFormattingPreview",
|
"TextComposerFormattingPreview",
|
||||||
"TextComposerLinkDialogCreateLinkPreview",
|
"TextComposerLinkDialogCreateLinkPreview",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue