diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt index b85ce2c135..72ea0a2098 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt @@ -9,6 +9,9 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.textcomposer.model.TextEditorState data class AttachmentsPreviewState( @@ -16,7 +19,11 @@ data class AttachmentsPreviewState( val sendActionState: SendActionState, val textEditorState: TextEditorState, val eventSink: (AttachmentsPreviewEvents) -> Unit -) +) { + val allowCaption: Boolean = (attachment as? Attachment.Media)?.localMedia?.info?.mimeType?.let { + it.isMimeTypeImage() || it.isMimeTypeVideo() + }.orFalse() +} @Immutable sealed interface SendActionState { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index ab7a117fdf..2906f26b3c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -176,7 +176,7 @@ private fun AttachmentsPreviewBottomActions( modifier = modifier, state = state.textEditorState, voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Caption, + composerMode = MessageComposerMode.Attachment(state.allowCaption), onRequestFocus = {}, onSendMessage = onSendClick, showTextFormatting = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 6f430cbaae..6101f48c1f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -436,7 +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.Attachment, is MessageComposerMode.Normal -> room.sendMessage( body = message.markdown, htmlBody = message.html, @@ -606,7 +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.Attachment, is MessageComposerMode.Normal -> ComposerDraftType.NewMessage is MessageComposerMode.Edit -> { mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 47f4b5268f..eca7340219 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -72,6 +72,7 @@ import io.element.android.wysiwyg.compose.RichTextEditorState import io.element.android.wysiwyg.display.TextDisplay import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import uniffi.wysiwyg_composer.MenuAction import kotlin.time.Duration.Companion.seconds @@ -120,12 +121,12 @@ fun TextComposer( } val layoutModifier = modifier - .fillMaxSize() - .height(IntrinsicSize.Min) + .fillMaxSize() + .height(IntrinsicSize.Min) val composerOptionsButton: @Composable () -> Unit = remember { @Composable { - if (composerMode == MessageComposerMode.Caption) { + if (composerMode is MessageComposerMode.Attachment) { Spacer(modifier = Modifier.width(9.dp)) } else { ComposerOptionsButton( @@ -139,54 +140,60 @@ fun TextComposer( val placeholder = if (composerMode.inThread) { stringResource(id = CommonStrings.action_reply_in_thread) - } else if (composerMode == MessageComposerMode.Caption) { + } else if (composerMode is MessageComposerMode.Attachment) { stringResource(id = R.string.rich_text_editor_composer_caption_placeholder) } else { stringResource(id = R.string.rich_text_editor_composer_placeholder) } - val textInput: @Composable () -> Unit = when (state) { - is TextEditorState.Rich -> { - remember(state.richTextEditorState, subcomposing, composerMode, onResetComposerMode, onError) { - @Composable { - TextInput( - state = state.richTextEditorState, - subcomposing = subcomposing, - placeholder = placeholder, - composerMode = composerMode, - onResetComposerMode = onResetComposerMode, - resolveMentionDisplay = resolveMentionDisplay, - resolveRoomMentionDisplay = { resolveMentionDisplay("@room", "#") }, - onError = onError, - onTyping = onTyping, - onSelectRichContent = onSelectRichContent, - ) + val textInput: @Composable () -> Unit = if ((composerMode as? MessageComposerMode.Attachment)?.allowCaption == false) { + { + // No text input when in attachment mode and caption not allowed. + } + } else { + when (state) { + is TextEditorState.Rich -> { + remember(state.richTextEditorState, subcomposing, composerMode, onResetComposerMode, onError) { + @Composable { + TextInput( + state = state.richTextEditorState, + subcomposing = subcomposing, + placeholder = placeholder, + composerMode = composerMode, + onResetComposerMode = onResetComposerMode, + resolveMentionDisplay = resolveMentionDisplay, + resolveRoomMentionDisplay = { resolveMentionDisplay("@room", "#") }, + onError = onError, + onTyping = onTyping, + onSelectRichContent = onSelectRichContent, + ) + } } } - } - is TextEditorState.Markdown -> { - @Composable { - val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus()) - TextInputBox( - composerMode = composerMode, - onResetComposerMode = onResetComposerMode, - placeholder = placeholder, - showPlaceholder = { state.state.text.value().isEmpty() }, - subcomposing = subcomposing, - ) { - MarkdownTextInput( - state = state.state, + is TextEditorState.Markdown -> { + @Composable { + val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus()) + TextInputBox( + composerMode = composerMode, + onResetComposerMode = onResetComposerMode, + placeholder = placeholder, + showPlaceholder = { state.state.text.value().isEmpty() }, subcomposing = subcomposing, - onTyping = onTyping, - onReceiveSuggestion = onReceiveSuggestion, - richTextEditorStyle = style, - onSelectRichContent = onSelectRichContent, - ) + ) { + MarkdownTextInput( + state = state.state, + subcomposing = subcomposing, + onTyping = onTyping, + onReceiveSuggestion = onReceiveSuggestion, + richTextEditorStyle = style, + onSelectRichContent = onSelectRichContent, + ) + } } } } } - val canSendMessage = markdown.isNotBlank() || composerMode == MessageComposerMode.Caption + val canSendMessage = markdown.isNotBlank() || composerMode is MessageComposerMode.Attachment val sendButton = @Composable { SendButton( canSendMessage = canSendMessage, @@ -324,8 +331,8 @@ private fun StandardLayout( if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) { Box( modifier = Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) - .size(48.dp), + .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) + .size(48.dp), contentAlignment = Alignment.Center, ) { voiceDeleteButton() @@ -335,8 +342,8 @@ private fun StandardLayout( } Box( modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) ) { voiceRecording() } @@ -349,16 +356,16 @@ private fun StandardLayout( } Box( modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) ) { textInput() } } Box( - Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) - .size(48.dp), + Modifier + .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) + .size(48.dp), contentAlignment = Alignment.Center, ) { endButton() @@ -380,8 +387,8 @@ private fun TextFormattingLayout( ) { Box( modifier = Modifier - .weight(1f) - .padding(horizontal = 12.dp) + .weight(1f) + .padding(horizontal = 12.dp) ) { textInput() } @@ -425,11 +432,11 @@ private fun TextInputBox( Column( modifier = Modifier - .clip(roundedCorners) - .border(0.5.dp, borderColor, roundedCorners) - .background(color = bgColor) - .requiredHeightIn(min = 42.dp) - .fillMaxSize(), + .clip(roundedCorners) + .border(0.5.dp, borderColor, roundedCorners) + .background(color = bgColor) + .requiredHeightIn(min = 42.dp) + .fillMaxSize(), ) { if (composerMode is MessageComposerMode.Special) { ComposerModeView( @@ -440,9 +447,9 @@ private fun TextInputBox( val defaultTypography = ElementTheme.typography.fontBodyLgRegular Box( modifier = Modifier - .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp) - // Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail - .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), + .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp) + // Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail + .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), contentAlignment = Alignment.CenterStart, ) { // Placeholder @@ -488,8 +495,8 @@ private fun TextInput( // This prevents it gaining focus and mutating the state. registerStateUpdates = !subcomposing, modifier = Modifier - .padding(top = 6.dp, bottom = 6.dp) - .fillMaxWidth(), + .padding(top = 6.dp, bottom = 6.dp) + .fillMaxWidth(), style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus), resolveMentionDisplay = resolveMentionDisplay, resolveRoomMentionDisplay = resolveRoomMentionDisplay, @@ -525,7 +532,7 @@ private fun aTextEditorStateRichList() = persistentListOf( internal fun TextComposerSimplePreview() = ElementPreview { PreviewColumn( items = aTextEditorStateMarkdownList() - ) { textEditorState -> + ) { _, textEditorState -> ATextComposer( state = textEditorState, voiceMessageState = VoiceMessageState.Idle, @@ -540,7 +547,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { internal fun TextComposerFormattingPreview() = ElementPreview { PreviewColumn( items = aTextEditorStateRichList() - ) { textEditorState -> + ) { _, textEditorState -> ATextComposer( state = textEditorState, voiceMessageState = VoiceMessageState.Idle, @@ -556,7 +563,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview { internal fun TextComposerEditPreview() = ElementPreview { PreviewColumn( items = aTextEditorStateRichList() - ) { textEditorState -> + ) { _, textEditorState -> ATextComposer( state = textEditorState, voiceMessageState = VoiceMessageState.Idle, @@ -571,7 +578,7 @@ internal fun TextComposerEditPreview() = ElementPreview { internal fun MarkdownTextComposerEditPreview() = ElementPreview { PreviewColumn( items = aTextEditorStateMarkdownList() - ) { textEditorState -> + ) { _, textEditorState -> ATextComposer( state = textEditorState, voiceMessageState = VoiceMessageState.Idle, @@ -586,7 +593,7 @@ internal fun MarkdownTextComposerEditPreview() = ElementPreview { internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview { PreviewColumn( items = aTextEditorStateRichList() - ) { textEditorState -> + ) { _, textEditorState -> ATextComposer( state = textEditorState, voiceMessageState = VoiceMessageState.Idle, @@ -601,13 +608,14 @@ internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider @PreviewsDayNight @Composable internal fun TextComposerCaptionPreview() = ElementPreview { + val list = aTextEditorStateMarkdownList() PreviewColumn( - items = aTextEditorStateMarkdownList() - ) { textEditorState -> + items = (list + aTextEditorStateMarkdown(initialText = "NO_CAPTION", initialFocus = true)).toPersistentList() + ) { index, textEditorState -> ATextComposer( state = textEditorState, voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Caption, + composerMode = MessageComposerMode.Attachment(allowCaption = index < list.size), enableVoiceMessages = false, ) } @@ -644,7 +652,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { playbackProgress = 0.0f ), ) - ) { voiceMessageState -> + ) { _, voiceMessageState -> ATextComposer( state = aTextEditorStateRich(initialFocus = true), voiceMessageState = voiceMessageState, @@ -657,14 +665,14 @@ internal fun TextComposerVoicePreview() = ElementPreview { @Composable private fun PreviewColumn( items: ImmutableList, - view: @Composable (T) -> Unit, + view: @Composable (Int, T) -> Unit, ) { Column { - items.forEach { item -> + items.forEachIndexed { index, item -> Box( modifier = Modifier.height(IntrinsicSize.Min) ) { - view(item) + view(index, item) } } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index a729f332b7..1915359c83 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.ui.messages.reply.eventId sealed interface MessageComposerMode { data object Normal : MessageComposerMode - data object Caption : MessageComposerMode + data class Attachment(val allowCaption: Boolean) : MessageComposerMode sealed interface Special : MessageComposerMode @@ -37,7 +37,7 @@ sealed interface MessageComposerMode { val relatedEventId: EventId? get() = when (this) { is Normal, - is Caption -> null + is Attachment -> null is Edit -> eventOrTransactionId.eventId is Reply -> eventId }