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) }