Skip to content

Commit 47d7eac

Browse files
authored
Merge pull request #3803 from element-hq/feature/bma/sendCaption
Send caption with image and video
2 parents 98bf685 + ddc4085 commit 47d7eac

File tree

27 files changed

+383
-127
lines changed

27 files changed

+383
-127
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@ package io.element.android.features.messages.impl.attachments.preview
99

1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.runtime.MutableState
12+
import androidx.compose.runtime.getValue
1213
import androidx.compose.runtime.mutableStateOf
1314
import androidx.compose.runtime.remember
1415
import androidx.compose.runtime.rememberCoroutineScope
16+
import androidx.compose.runtime.rememberUpdatedState
1517
import dagger.assisted.Assisted
1618
import dagger.assisted.AssistedFactory
1719
import dagger.assisted.AssistedInject
1820
import io.element.android.features.messages.impl.attachments.Attachment
1921
import io.element.android.libraries.architecture.Presenter
2022
import io.element.android.libraries.matrix.api.core.ProgressCallback
23+
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
2124
import io.element.android.libraries.mediaupload.api.MediaSender
25+
import io.element.android.libraries.textcomposer.model.TextEditorState
26+
import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState
2227
import kotlinx.coroutines.CancellationException
2328
import kotlinx.coroutines.CoroutineScope
2429
import kotlinx.coroutines.Job
@@ -30,6 +35,7 @@ import kotlin.coroutines.coroutineContext
3035
class AttachmentsPreviewPresenter @AssistedInject constructor(
3136
@Assisted private val attachment: Attachment,
3237
private val mediaSender: MediaSender,
38+
private val permalinkBuilder: PermalinkBuilder,
3339
) : Presenter<AttachmentsPreviewState> {
3440
@AssistedFactory
3541
interface Factory {
@@ -44,11 +50,24 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
4450
mutableStateOf<SendActionState>(SendActionState.Idle)
4551
}
4652

53+
val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false)
54+
val textEditorState by rememberUpdatedState(
55+
TextEditorState.Markdown(markdownTextEditorState)
56+
)
57+
4758
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
4859

4960
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
5061
when (attachmentsPreviewEvents) {
51-
AttachmentsPreviewEvents.SendAttachment -> ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(attachment, sendActionState)
62+
is AttachmentsPreviewEvents.SendAttachment -> {
63+
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
64+
.takeIf { it.isNotEmpty() }
65+
ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(
66+
attachment = attachment,
67+
caption = caption,
68+
sendActionState = sendActionState,
69+
)
70+
}
5271
AttachmentsPreviewEvents.ClearSendState -> {
5372
ongoingSendAttachmentJob.value?.let {
5473
it.cancel()
@@ -62,18 +81,21 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
6281
return AttachmentsPreviewState(
6382
attachment = attachment,
6483
sendActionState = sendActionState.value,
84+
textEditorState = textEditorState,
6585
eventSink = ::handleEvents
6686
)
6787
}
6888

6989
private fun CoroutineScope.sendAttachment(
7090
attachment: Attachment,
91+
caption: String?,
7192
sendActionState: MutableState<SendActionState>,
7293
) = launch {
7394
when (attachment) {
7495
is Attachment.Media -> {
7596
sendMedia(
7697
mediaAttachment = attachment,
98+
caption = caption,
7799
sendActionState = sendActionState,
78100
)
79101
}
@@ -82,6 +104,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
82104

83105
private suspend fun sendMedia(
84106
mediaAttachment: Attachment.Media,
107+
caption: String?,
85108
sendActionState: MutableState<SendActionState>,
86109
) = runCatching {
87110
val context = coroutineContext
@@ -96,6 +119,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
96119
mediaSender.sendMedia(
97120
uri = mediaAttachment.localMedia.uri,
98121
mimeType = mediaAttachment.localMedia.info.mimeType,
122+
caption = caption,
99123
progressCallback = progressCallback
100124
).getOrThrow()
101125
}.fold(

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@ package io.element.android.features.messages.impl.attachments.preview
99

1010
import androidx.compose.runtime.Immutable
1111
import io.element.android.features.messages.impl.attachments.Attachment
12+
import io.element.android.libraries.core.bool.orFalse
13+
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
14+
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
15+
import io.element.android.libraries.textcomposer.model.TextEditorState
1216

1317
data class AttachmentsPreviewState(
1418
val attachment: Attachment,
1519
val sendActionState: SendActionState,
20+
val textEditorState: TextEditorState,
1621
val eventSink: (AttachmentsPreviewEvents) -> Unit
17-
)
22+
) {
23+
val allowCaption: Boolean = (attachment as? Attachment.Media)?.localMedia?.info?.mimeType?.let {
24+
it.isMimeTypeImage() || it.isMimeTypeVideo()
25+
}.orFalse()
26+
}
1827

1928
@Immutable
2029
sealed interface SendActionState {

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@ import androidx.core.net.toUri
1212
import io.element.android.features.messages.impl.attachments.Attachment
1313
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
1414
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
15+
import io.element.android.libraries.mediaviewer.api.local.aVideoMediaInfo
1516
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
17+
import io.element.android.libraries.mediaviewer.api.local.anAudioMediaInfo
1618
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
19+
import io.element.android.libraries.textcomposer.model.TextEditorState
20+
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
1721

1822
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
1923
override val values: Sequence<AttachmentsPreviewState>
2024
get() = sequenceOf(
2125
anAttachmentsPreviewState(),
26+
anAttachmentsPreviewState(mediaInfo = aVideoMediaInfo()),
27+
anAttachmentsPreviewState(mediaInfo = anAudioMediaInfo()),
2228
anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()),
2329
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
2430
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
@@ -27,11 +33,13 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
2733

2834
fun anAttachmentsPreviewState(
2935
mediaInfo: MediaInfo = anImageMediaInfo(),
30-
sendActionState: SendActionState = SendActionState.Idle
36+
textEditorState: TextEditorState = aTextEditorStateMarkdown(),
37+
sendActionState: SendActionState = SendActionState.Idle,
3138
) = AttachmentsPreviewState(
3239
attachment = Attachment.Media(
3340
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
3441
),
3542
sendActionState = sendActionState,
43+
textEditorState = textEditorState,
3644
eventSink = {}
3745
)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,37 +9,44 @@ package io.element.android.features.messages.impl.attachments.preview
99

1010
import androidx.compose.foundation.background
1111
import androidx.compose.foundation.layout.Box
12-
import androidx.compose.foundation.layout.defaultMinSize
12+
import androidx.compose.foundation.layout.IntrinsicSize
1313
import androidx.compose.foundation.layout.fillMaxSize
1414
import androidx.compose.foundation.layout.fillMaxWidth
15+
import androidx.compose.foundation.layout.height
16+
import androidx.compose.foundation.layout.imePadding
1517
import androidx.compose.foundation.layout.navigationBarsPadding
16-
import androidx.compose.foundation.layout.padding
18+
import androidx.compose.material3.ExperimentalMaterial3Api
1719
import androidx.compose.runtime.Composable
1820
import androidx.compose.runtime.LaunchedEffect
1921
import androidx.compose.runtime.getValue
2022
import androidx.compose.runtime.rememberUpdatedState
2123
import androidx.compose.ui.Alignment
2224
import androidx.compose.ui.Modifier
23-
import androidx.compose.ui.graphics.Color
2425
import androidx.compose.ui.res.stringResource
2526
import androidx.compose.ui.tooling.preview.Preview
2627
import androidx.compose.ui.tooling.preview.PreviewParameter
27-
import androidx.compose.ui.unit.dp
28+
import io.element.android.compound.theme.ElementTheme
29+
import io.element.android.compound.tokens.generated.CompoundIcons
2830
import io.element.android.features.messages.impl.attachments.Attachment
2931
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
30-
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
3132
import io.element.android.libraries.designsystem.components.ProgressDialog
3233
import io.element.android.libraries.designsystem.components.ProgressDialogType
34+
import io.element.android.libraries.designsystem.components.button.BackButton
3335
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
3436
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
3537
import io.element.android.libraries.designsystem.theme.components.Scaffold
36-
import io.element.android.libraries.designsystem.theme.components.TextButton
38+
import io.element.android.libraries.designsystem.theme.components.TopAppBar
3739
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
3840
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
41+
import io.element.android.libraries.textcomposer.TextComposer
42+
import io.element.android.libraries.textcomposer.model.MessageComposerMode
43+
import io.element.android.libraries.textcomposer.model.VoiceMessageState
3944
import io.element.android.libraries.ui.strings.CommonStrings
45+
import io.element.android.wysiwyg.display.TextDisplay
4046
import me.saket.telephoto.zoomable.ZoomSpec
4147
import me.saket.telephoto.zoomable.rememberZoomableState
4248

49+
@OptIn(ExperimentalMaterial3Api::class)
4350
@Composable
4451
fun AttachmentsPreviewView(
4552
state: AttachmentsPreviewState,
@@ -61,11 +68,23 @@ fun AttachmentsPreviewView(
6168
}
6269
}
6370

64-
Scaffold(modifier) {
71+
Scaffold(
72+
modifier = modifier,
73+
topBar = {
74+
TopAppBar(
75+
navigationIcon = {
76+
BackButton(
77+
imageVector = CompoundIcons.Close(),
78+
onClick = onDismiss,
79+
)
80+
},
81+
title = {},
82+
)
83+
}
84+
) {
6585
AttachmentPreviewContent(
66-
attachment = state.attachment,
86+
state = state,
6787
onSendClick = ::postSendAttachment,
68-
onDismiss = onDismiss
6988
)
7089
}
7190
AttachmentSendStateView(
@@ -106,21 +125,19 @@ private fun AttachmentSendStateView(
106125

107126
@Composable
108127
private fun AttachmentPreviewContent(
109-
attachment: Attachment,
128+
state: AttachmentsPreviewState,
110129
onSendClick: () -> Unit,
111-
onDismiss: () -> Unit,
112130
) {
113131
Box(
114132
modifier = Modifier
115133
.fillMaxSize()
116134
.navigationBarsPadding(),
117-
contentAlignment = Alignment.BottomCenter
118135
) {
119136
Box(
120137
modifier = Modifier.fillMaxSize(),
121138
contentAlignment = Alignment.Center
122139
) {
123-
when (attachment) {
140+
when (val attachment = state.attachment) {
124141
is Attachment.Media -> {
125142
val localMediaViewState = rememberLocalMediaViewState(
126143
zoomableState = rememberZoomableState(
@@ -137,27 +154,46 @@ private fun AttachmentPreviewContent(
137154
}
138155
}
139156
AttachmentsPreviewBottomActions(
140-
onCancelClick = onDismiss,
157+
state = state,
141158
onSendClick = onSendClick,
142159
modifier = Modifier
143160
.fillMaxWidth()
144-
.background(Color.Black.copy(alpha = 0.7f))
145-
.padding(horizontal = 24.dp)
146-
.defaultMinSize(minHeight = 80.dp)
161+
.background(ElementTheme.colors.bgCanvasDefault)
162+
.height(IntrinsicSize.Min)
163+
.align(Alignment.BottomCenter)
164+
.imePadding(),
147165
)
148166
}
149167
}
150168

151169
@Composable
152170
private fun AttachmentsPreviewBottomActions(
153-
onCancelClick: () -> Unit,
171+
state: AttachmentsPreviewState,
154172
onSendClick: () -> Unit,
155173
modifier: Modifier = Modifier
156174
) {
157-
ButtonRowMolecule(modifier = modifier) {
158-
TextButton(stringResource(id = CommonStrings.action_cancel), onClick = onCancelClick)
159-
TextButton(stringResource(id = CommonStrings.action_send), onClick = onSendClick)
160-
}
175+
TextComposer(
176+
modifier = modifier,
177+
state = state.textEditorState,
178+
voiceMessageState = VoiceMessageState.Idle,
179+
composerMode = MessageComposerMode.Attachment(state.allowCaption),
180+
onRequestFocus = {},
181+
onSendMessage = onSendClick,
182+
showTextFormatting = false,
183+
onResetComposerMode = {},
184+
onAddAttachment = {},
185+
onDismissTextFormatting = {},
186+
enableVoiceMessages = false,
187+
onVoiceRecorderEvent = {},
188+
onVoicePlayerEvent = {},
189+
onSendVoiceMessage = {},
190+
onDeleteVoiceMessage = {},
191+
onReceiveSuggestion = {},
192+
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
193+
onError = {},
194+
onTyping = {},
195+
onSelectRichContent = {},
196+
)
161197
}
162198

163199
// Only preview in dark, dark theme is forced on the Node.

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ class MessageComposerPresenter @Inject constructor(
436436
// Reset composer right away
437437
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
438438
when (capturedMode) {
439+
is MessageComposerMode.Attachment,
439440
is MessageComposerMode.Normal -> room.sendMessage(
440441
body = message.markdown,
441442
htmlBody = message.html,
@@ -605,6 +606,7 @@ class MessageComposerPresenter @Inject constructor(
605606
): ComposerDraft? {
606607
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = false)
607608
val draftType = when (val mode = messageComposerContext.composerMode) {
609+
is MessageComposerMode.Attachment,
608610
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
609611
is MessageComposerMode.Edit -> {
610612
mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }

0 commit comments

Comments
 (0)