diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 4fd59be6cb..75b6021992 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -38,8 +38,6 @@ import io.element.android.features.location.api.Location import io.element.android.features.location.api.SendLocationEntryPoint import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint -import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode import io.element.android.features.messages.impl.forward.ForwardMessagesNode import io.element.android.features.messages.impl.report.ReportMessageNode import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode @@ -77,7 +75,6 @@ import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction -import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize @@ -124,9 +121,6 @@ class MessagesFlowNode @AssistedInject constructor( val thumbnailSource: MediaSource?, ) : NavTarget - @Parcelize - data class AttachmentPreview(val attachment: Attachment) : NavTarget - @Parcelize data class LocationViewer(val location: Location, val description: String?) : NavTarget @@ -173,10 +167,6 @@ class MessagesFlowNode @AssistedInject constructor( return processEventClick(event) } - override fun onPreviewAttachments(attachments: ImmutableList) { - backstack.push(NavTarget.AttachmentPreview(attachments.first())) - } - override fun onUserDataClick(userId: UserId) { callbacks.forEach { it.onUserDataClick(userId) } } @@ -233,10 +223,6 @@ class MessagesFlowNode @AssistedInject constructor( ) createNode(buildContext, listOf(inputs)) } - is NavTarget.AttachmentPreview -> { - val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment) - createNode(buildContext, listOf(inputs)) - } is NavTarget.LocationViewer -> { val inputs = ShowLocationEntryPoint.Inputs(navTarget.location, navTarget.description) showLocationEntryPoint.createNode(this, buildContext, inputs) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index b2ee1053a6..c8b6621545 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -35,7 +35,6 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineEvents @@ -61,7 +60,6 @@ import io.element.android.libraries.matrix.api.room.alias.matches import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.services.analytics.api.AnalyticsService -import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) class MessagesNode @AssistedInject constructor( @@ -87,7 +85,6 @@ class MessagesNode @AssistedInject constructor( interface Callback : Plugin { fun onRoomDetailsClick() fun onEventClick(event: TimelineItem.Event): Boolean - fun onPreviewAttachments(attachments: ImmutableList) fun onUserDataClick(userId: UserId) fun onPermalinkClick(data: PermalinkData) fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) @@ -126,10 +123,6 @@ class MessagesNode @AssistedInject constructor( .orFalse() } - private fun onPreviewAttachments(attachments: ImmutableList) { - callbacks.forEach { it.onPreviewAttachments(attachments) } - } - private fun onUserDataClick(userId: UserId) { callbacks.forEach { it.onUserDataClick(userId) } } @@ -215,7 +208,6 @@ class MessagesNode @AssistedInject constructor( onBackClick = this::navigateUp, onRoomDetailsClick = this::onRoomDetailsClick, onEventClick = this::onEventClick, - onPreviewAttachments = this::onPreviewAttachments, onUserDataClick = this::onUserDataClick, onLinkClick = { onLinkClick(context, it, state.timelineState.eventSink) }, onSendLocationClick = this::onSendLocationClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 24439c0c75..2cf8149117 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -38,10 +38,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -62,7 +60,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewView import io.element.android.features.messages.impl.mentions.MentionSuggestionsPickerView import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet import io.element.android.features.messages.impl.messagecomposer.AttachmentsState @@ -116,7 +114,6 @@ fun MessagesView( onEventClick: (event: TimelineItem.Event) -> Boolean, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, - onPreviewAttachments: (ImmutableList) -> Unit, onSendLocationClick: () -> Unit, onCreatePollClick: () -> Unit, onJoinCallClick: () -> Unit, @@ -131,7 +128,6 @@ fun MessagesView( AttachmentStateView( state = state.composerState.attachmentsState, - onPreviewAttachments = onPreviewAttachments, onCancel = { state.composerState.eventSink(MessageComposerEvents.CancelSendAttachment) }, ) @@ -277,17 +273,11 @@ private fun ReinviteDialog(state: MessagesState) { @Composable private fun AttachmentStateView( state: AttachmentsState, - onPreviewAttachments: (ImmutableList) -> Unit, onCancel: () -> Unit, ) { when (state) { AttachmentsState.None -> Unit - is AttachmentsState.Previewing -> { - val latestOnPreviewAttachments by rememberUpdatedState(onPreviewAttachments) - LaunchedEffect(state) { - latestOnPreviewAttachments(state.attachments) - } - } + is AttachmentsState.Previewing -> Unit is AttachmentsState.Sending -> { ProgressDialog( type = when (state) { @@ -398,7 +388,11 @@ private fun MessagesViewContent( }, sheetContentKey = sheetResizeContentKey.intValue, sheetTonalElevation = 0.dp, - sheetShadowElevation = if (state.composerState.memberSuggestions.isNotEmpty()) 16.dp else 0.dp, + sheetShadowElevation = + if (state.composerState.memberSuggestions.isNotEmpty() || state.composerState.attachmentsState != AttachmentsState.None) + 16.dp + else + 0.dp, ) } } @@ -427,6 +421,14 @@ private fun MessagesViewComposerBottomSheetContents( state.composerState.eventSink(MessageComposerEvents.InsertMention(it)) } ) + AttachmentsPreviewView( + state = state.composerState.attachmentsState, + onDismiss = { + state.composerState.eventSink(MessageComposerEvents.ClearAttachments) + }, + modifier = Modifier + .heightIn(max = 230.dp) + ) MessageComposerView( state = state.composerState, voiceMessageState = state.voiceMessageComposerState, @@ -557,7 +559,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onBackClick = {}, onRoomDetailsClick = {}, onEventClick = { false }, - onPreviewAttachments = {}, onUserDataClick = {}, onLinkClick = {}, onSendLocationClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt deleted file mode 100644 index 6ce9348fcb..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.messages.impl.attachments.preview - -import androidx.compose.runtime.Immutable - -@Immutable -sealed interface AttachmentsPreviewEvents { - data object SendAttachment : AttachmentsPreviewEvents - data object ClearSendState : AttachmentsPreviewEvents -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt deleted file mode 100644 index 254642b62c..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.messages.impl.attachments.preview - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.compound.theme.ForcedDarkElementTheme -import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.RoomScope - -@ContributesNode(RoomScope::class) -class AttachmentsPreviewNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - presenterFactory: AttachmentsPreviewPresenter.Factory, -) : Node(buildContext, plugins = plugins) { - data class Inputs(val attachment: Attachment) : NodeInputs - - private val inputs: Inputs = inputs() - - private val presenter = presenterFactory.create(inputs.attachment) - - @Composable - override fun View(modifier: Modifier) { - ForcedDarkElementTheme { - val state = presenter.present() - AttachmentsPreviewView( - state = state, - onDismiss = this::navigateUp, - modifier = modifier - ) - } - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt deleted file mode 100644 index 2c5a3ccf08..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.messages.impl.attachments.preview - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -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.mediaupload.api.MediaSender -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import timber.log.Timber -import kotlin.coroutines.coroutineContext - -class AttachmentsPreviewPresenter @AssistedInject constructor( - @Assisted private val attachment: Attachment, - private val mediaSender: MediaSender, -) : Presenter { - @AssistedFactory - interface Factory { - fun create(attachment: Attachment): AttachmentsPreviewPresenter - } - - @Composable - override fun present(): AttachmentsPreviewState { - val coroutineScope = rememberCoroutineScope() - - val sendActionState = remember { - mutableStateOf(SendActionState.Idle) - } - - val ongoingSendAttachmentJob = remember { mutableStateOf(null) } - - fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) { - when (attachmentsPreviewEvents) { - AttachmentsPreviewEvents.SendAttachment -> ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(attachment, sendActionState) - AttachmentsPreviewEvents.ClearSendState -> { - ongoingSendAttachmentJob.value?.let { - it.cancel() - ongoingSendAttachmentJob.value = null - } - sendActionState.value = SendActionState.Idle - } - } - } - - return AttachmentsPreviewState( - attachment = attachment, - sendActionState = sendActionState.value, - eventSink = ::handleEvents - ) - } - - private fun CoroutineScope.sendAttachment( - attachment: Attachment, - sendActionState: MutableState, - ) = launch { - when (attachment) { - is Attachment.Media -> { - sendMedia( - mediaAttachment = attachment, - sendActionState = sendActionState, - ) - } - } - } - - private suspend fun sendMedia( - mediaAttachment: Attachment.Media, - sendActionState: MutableState, - ) = runCatching { - val context = coroutineContext - val progressCallback = object : ProgressCallback { - override fun onProgress(current: Long, total: Long) { - if (context.isActive) { - sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat()) - } - } - } - sendActionState.value = SendActionState.Sending.Processing - mediaSender.sendMedia( - uri = mediaAttachment.localMedia.uri, - mimeType = mediaAttachment.localMedia.info.mimeType, - compressIfPossible = mediaAttachment.compressIfPossible, - progressCallback = progressCallback - ).getOrThrow() - }.fold( - onSuccess = { - sendActionState.value = SendActionState.Done - }, - onFailure = { error -> - Timber.e(error, "Failed to send attachment") - if (error is CancellationException) { - throw error - } else { - sendActionState.value = SendActionState.Failure(error) - } - } - ) -} 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 deleted file mode 100644 index 8b013f165a..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.messages.impl.attachments.preview - -import androidx.compose.runtime.Immutable -import io.element.android.features.messages.impl.attachments.Attachment - -data class AttachmentsPreviewState( - val attachment: Attachment, - val sendActionState: SendActionState, - val eventSink: (AttachmentsPreviewEvents) -> Unit -) - -@Immutable -sealed interface SendActionState { - data object Idle : SendActionState - - @Immutable - sealed interface Sending : SendActionState { - data object Processing : Sending - data class Uploading(val progress: Float) : Sending - } - - data class Failure(val error: Throwable) : SendActionState - data object Done : SendActionState -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index 8dbf067b95..448fd2008b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -19,29 +19,24 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.core.net.toUri import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.messagecomposer.AttachmentsState 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 kotlinx.collections.immutable.toImmutableList -open class AttachmentsPreviewStateProvider : PreviewParameterProvider { - override val values: Sequence +open class AttachmentsPreviewStateProvider : PreviewParameterProvider { + val attachmentList = + mutableListOf( + Attachment.Media( + localMedia = LocalMedia("file://path".toUri(), anApkMediaInfo()), + compressIfPossible = true) + ).toImmutableList() + override val values: Sequence get() = sequenceOf( - anAttachmentsPreviewState(), - anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()), - anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)), - anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))), + AttachmentsState.None, + AttachmentsState.Previewing(attachmentList), + AttachmentsState.Sending.Processing(attachmentList), + AttachmentsState.Sending.Uploading(25.0F) ) } -fun anAttachmentsPreviewState( - mediaInfo: MediaInfo = anImageMediaInfo(), - sendActionState: SendActionState = SendActionState.Idle -) = AttachmentsPreviewState( - attachment = Attachment.Media( - localMedia = LocalMedia("file://path".toUri(), mediaInfo), - compressIfPossible = true - ), - sendActionState = sendActionState, - eventSink = {} -) 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 aaacdbac2d..90156df38e 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 @@ -16,98 +16,46 @@ 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.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding +import androidx.compose.material3.FabPosition 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.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.dialogs.RetryDialog +import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.theme.components.FloatingActionButton +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.mediaviewer.api.local.LocalMediaView import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState -import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.rememberZoomableState @Composable fun AttachmentsPreviewView( - state: AttachmentsPreviewState, + state: AttachmentsState, onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - fun postSendAttachment() { - state.eventSink(AttachmentsPreviewEvents.SendAttachment) - } - - fun postClearSendState() { - state.eventSink(AttachmentsPreviewEvents.ClearSendState) - } - - if (state.sendActionState is SendActionState.Done) { - val latestOnDismiss by rememberUpdatedState(onDismiss) - LaunchedEffect(state.sendActionState) { - latestOnDismiss() - } - } - - Scaffold(modifier) { - AttachmentPreviewContent( - attachment = state.attachment, - onSendClick = ::postSendAttachment, - onDismiss = onDismiss - ) - } - AttachmentSendStateView( - sendActionState = state.sendActionState, - onDismissClick = ::postClearSendState, - onRetryClick = ::postSendAttachment - ) -} - -@Composable -private fun AttachmentSendStateView( - sendActionState: SendActionState, - onDismissClick: () -> Unit, - onRetryClick: () -> Unit -) { - when (sendActionState) { - is SendActionState.Sending -> { - ProgressDialog( - type = when (sendActionState) { - is SendActionState.Sending.Uploading -> ProgressDialogType.Determinate(sendActionState.progress) - SendActionState.Sending.Processing -> ProgressDialogType.Indeterminate + when (state) { + is AttachmentsState.Previewing -> { + Scaffold(modifier, + floatingActionButton = { + FloatingActionButton(onClick = onDismiss) { Icon(imageVector = CompoundIcons.Close(), contentDescription = null) } }, - text = stringResource(id = CommonStrings.common_sending), - showCancelButton = true, - onDismissRequest = onDismissClick, - ) - } - is SendActionState.Failure -> { - RetryDialog( - content = stringResource(sendAttachmentError(sendActionState.error)), - onDismiss = onDismissClick, - onRetry = onRetryClick - ) + floatingActionButtonPosition = FabPosition.Start + ) { + AttachmentPreviewContent( + attachments = state.attachments, + ) + } } else -> Unit } @@ -115,9 +63,7 @@ private fun AttachmentSendStateView( @Composable private fun AttachmentPreviewContent( - attachment: Attachment, - onSendClick: () -> Unit, - onDismiss: () -> Unit, + attachments: ImmutableList, ) { Box( modifier = Modifier @@ -129,7 +75,7 @@ private fun AttachmentPreviewContent( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - when (attachment) { + when (val attachment = attachments.first()) { is Attachment.Media -> { val localMediaViewState = rememberLocalMediaViewState( zoomableState = rememberZoomableState( @@ -145,34 +91,13 @@ private fun AttachmentPreviewContent( } } } - AttachmentsPreviewBottomActions( - onCancelClick = onDismiss, - onSendClick = onSendClick, - modifier = Modifier - .fillMaxWidth() - .background(Color.Black.copy(alpha = 0.7f)) - .padding(horizontal = 24.dp) - .defaultMinSize(minHeight = 80.dp) - ) - } -} - -@Composable -private fun AttachmentsPreviewBottomActions( - onCancelClick: () -> Unit, - onSendClick: () -> Unit, - modifier: Modifier = Modifier -) { - ButtonRowMolecule(modifier = modifier) { - TextButton(stringResource(id = CommonStrings.action_cancel), onClick = onCancelClick) - TextButton(stringResource(id = CommonStrings.action_send), onClick = onSendClick) } } // Only preview in dark, dark theme is forced on the Node. @Preview @Composable -internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = ElementPreviewDark { +internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsState) = ElementPreviewDark { AttachmentsPreviewView( state = state, onDismiss = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt index 8ba8ceec01..c17fabbf21 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -43,6 +43,7 @@ import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.ListItemStyle import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.textcomposer.model.MessageComposerMode @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -106,51 +107,54 @@ private fun AttachmentSourcePickerMenu( .navigationBarsPadding() .imePadding() ) { - ListItem( - modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.TakePhoto())), - headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) }, - style = ListItemStyle.Primary, - ) - ListItem( - modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VideoCall())), - headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) }, - style = ListItemStyle.Primary, - ) - ListItem( - modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Image())), - headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) }, - style = ListItemStyle.Primary, - ) - ListItem( - modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Attachment())), - headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_files)) }, - style = ListItemStyle.Primary, - ) - if (state.canShareLocation) { + if (state.attachmentsState == AttachmentsState.None && + state.mode == MessageComposerMode.Normal) { ListItem( - modifier = Modifier.clickable { - state.eventSink(MessageComposerEvents.PickAttachmentSource.Location) - onSendLocationClick() - }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.LocationPin())), - headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_location)) }, + modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.TakePhoto())), + headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) }, + style = ListItemStyle.Primary, + ) + ListItem( + modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VideoCall())), + headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) }, + style = ListItemStyle.Primary, + ) + ListItem( + modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Image())), + headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) }, style = ListItemStyle.Primary, ) - } - if (state.canCreatePoll) { ListItem( - modifier = Modifier.clickable { - state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll) - onCreatePollClick() - }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Polls())), - headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_poll)) }, + modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Attachment())), + headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_files)) }, style = ListItemStyle.Primary, ) + if (state.canShareLocation) { + ListItem( + modifier = Modifier.clickable { + state.eventSink(MessageComposerEvents.PickAttachmentSource.Location) + onSendLocationClick() + }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.LocationPin())), + headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_location)) }, + style = ListItemStyle.Primary, + ) + } + if (state.canCreatePoll) { + ListItem( + modifier = Modifier.clickable { + state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll) + onCreatePollClick() + }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Polls())), + headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_poll)) }, + style = ListItemStyle.Primary, + ) + } } if (enableTextFormatting) { ListItem( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index 1f6ae7c7f4..69bc21e246 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -41,6 +41,7 @@ sealed interface MessageComposerEvents { } data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents data object CancelSendAttachment : MessageComposerEvents + data object ClearAttachments : MessageComposerEvents data class Error(val error: Throwable) : MessageComposerEvents data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents 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 d0e50d114c..8dbb7b20ec 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 @@ -189,12 +189,22 @@ class MessageComposerPresenter @Inject constructor( val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true) + val textEditorState by rememberUpdatedState( + if (showTextFormatting) { + TextEditorState.Rich(richTextEditorState) + } else { + TextEditorState.Markdown(markdownTextEditorState) + } + ) + LaunchedEffect(attachmentsState.value) { when (val attachmentStateValue = attachmentsState.value) { is AttachmentsState.Sending.Processing -> { ongoingSendAttachmentJob.value = localCoroutineScope.sendAttachment( - attachmentStateValue.attachments.first(), - attachmentsState, + attachment = attachmentStateValue.attachments.first(), + markdownTextEditorState = markdownTextEditorState, + attachmentState = attachmentsState, + richTextEditorState = richTextEditorState, ) } else -> Unit @@ -253,14 +263,6 @@ class MessageComposerPresenter @Inject constructor( } } - val textEditorState by rememberUpdatedState( - if (showTextFormatting) { - TextEditorState.Rich(richTextEditorState) - } else { - TextEditorState.Markdown(markdownTextEditorState) - } - ) - LaunchedEffect(Unit) { val draft = draftService.loadDraft(room.roomId, isVolatile = false) if (draft != null) { @@ -281,10 +283,24 @@ class MessageComposerPresenter @Inject constructor( } } is MessageComposerEvents.SendMessage -> { - appCoroutineScope.sendMessage( - markdownTextEditorState = markdownTextEditorState, - richTextEditorState = richTextEditorState, - ) + when (val attachmentState = attachmentsState.value) { + // Are we sending media? + is AttachmentsState.Previewing -> { + appCoroutineScope.sendAttachment( + attachmentState.attachments.first(), + markdownTextEditorState = markdownTextEditorState, + attachmentState = attachmentsState, + richTextEditorState = richTextEditorState, + ) + } + // Send normal message + else -> { + appCoroutineScope.sendMessage( + markdownTextEditorState = markdownTextEditorState, + richTextEditorState = richTextEditorState, + ) + } + } } is MessageComposerEvents.SendUri -> appCoroutineScope.sendAttachment( attachment = Attachment.Media( @@ -296,7 +312,9 @@ class MessageComposerPresenter @Inject constructor( ), compressIfPossible = true ), + markdownTextEditorState = markdownTextEditorState, attachmentState = attachmentsState, + richTextEditorState = richTextEditorState, ) is MessageComposerEvents.SetMode -> { localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState) @@ -339,6 +357,9 @@ class MessageComposerPresenter @Inject constructor( showAttachmentSourcePicker = false // Navigation to the create poll screen is done at the view layer } + is MessageComposerEvents.ClearAttachments -> { + attachmentsState.value = AttachmentsState.None + } is MessageComposerEvents.CancelSendAttachment -> { ongoingSendAttachmentJob.value?.let { it.cancel() @@ -467,19 +488,34 @@ class MessageComposerPresenter @Inject constructor( private fun CoroutineScope.sendAttachment( attachment: Attachment, + markdownTextEditorState: MarkdownTextEditorState, attachmentState: MutableState, - ) = when (attachment) { - is Attachment.Media -> { - launch { + richTextEditorState: RichTextEditorState, + ) = launch { + when (attachment) { + is Attachment.Media -> { + val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = true) + val capturedMode = messageComposerContext.composerMode + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit) sendMedia( - uri = attachment.localMedia.uri, - mimeType = attachment.localMedia.info.mimeType, - attachmentState = attachmentState, - ) + uri = attachment.localMedia.uri, + mimeType = attachment.localMedia.info.mimeType, + caption = when (message.markdown.isEmpty()) { + true -> null + false -> message.markdown + }, + formattedCaption = when (message.html.isNullOrEmpty()) { + true -> null + false -> message.html + }, + attachmentState = attachmentState, + compressIfPossible = attachment.compressIfPossible, + ) } } } + @UnstableApi private fun handlePickedMedia( attachmentsState: MutableState, @@ -514,7 +550,10 @@ class MessageComposerPresenter @Inject constructor( private suspend fun sendMedia( uri: Uri, mimeType: String, + caption: String?, + formattedCaption: String?, attachmentState: MutableState, + compressIfPossible: Boolean = false, ) = runCatching { val context = coroutineContext val progressCallback = object : ProgressCallback { @@ -524,10 +563,13 @@ class MessageComposerPresenter @Inject constructor( } } } + attachmentState.value = AttachmentsState.Sending.Uploading(0F) mediaSender.sendMedia( uri = uri, mimeType = mimeType, - compressIfPossible = false, + compressIfPossible = compressIfPossible, + caption, + formattedCaption, progressCallback = progressCallback ).getOrThrow() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 6e261e8ac9..a82559a8f0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -125,6 +125,7 @@ internal fun MessageComposerView( onError = ::onError, onTyping = ::onTyping, onSelectRichContent = ::sendUri, + hasAttachments = state.attachmentsState is AttachmentsState.Previewing, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt index c4a8a88153..8024d4d8ec 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt @@ -33,7 +33,6 @@ internal fun MessagesViewWithTypingPreview( onBackClick = {}, onRoomDetailsClick = {}, onEventClick = { false }, - onPreviewAttachments = {}, onUserDataClick = {}, onLinkClick = {}, onSendLocationClick = {}, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt deleted file mode 100644 index e2fbeebe3a..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package io.element.android.features.messages.impl.attachments - -import android.net.Uri -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents -import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter -import io.element.android.features.messages.impl.attachments.preview.SendActionState -import io.element.android.libraries.matrix.api.core.ProgressCallback -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler -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 -import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor -import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia -import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class AttachmentsPreviewPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - private val mediaPreProcessor = FakeMediaPreProcessor() - private val mockMediaUrl: Uri = mockk("localMediaUri") - - @Test - fun `present - send media success scenario`() = runTest { - val sendMediaResult = lambdaRecorder> { - Result.success(FakeMediaUploadHandler()) - } - val room = FakeMatrixRoom( - progressCallbackValues = listOf( - Pair(0, 10), - Pair(5, 10), - Pair(10, 10) - ), - sendMediaResult = sendMediaResult, - ) - val presenter = createAttachmentsPreviewPresenter(room = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) - initialState.eventSink(AttachmentsPreviewEvents.SendAttachment) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f)) - val successState = awaitItem() - assertThat(successState.sendActionState).isEqualTo(SendActionState.Done) - sendMediaResult.assertions().isCalledOnce() - } - } - - @Test - fun `present - send media failure scenario`() = runTest { - val failure = MediaPreProcessor.Failure(null) - val sendMediaResult = lambdaRecorder> { - Result.failure(failure) - } - val room = FakeMatrixRoom( - sendMediaResult = sendMediaResult, - ) - val presenter = createAttachmentsPreviewPresenter(room = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) - initialState.eventSink(AttachmentsPreviewEvents.SendAttachment) - val loadingState = awaitItem() - assertThat(loadingState.sendActionState).isEqualTo(SendActionState.Sending.Processing) - val failureState = awaitItem() - assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure)) - sendMediaResult.assertions().isCalledOnce() - failureState.eventSink(AttachmentsPreviewEvents.ClearSendState) - val clearedState = awaitItem() - assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Idle) - } - } - - @Test - fun `present - dismissing the progress dialog stops media upload`() = runTest { - val presenter = createAttachmentsPreviewPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) - initialState.eventSink(AttachmentsPreviewEvents.SendAttachment) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing) - initialState.eventSink(AttachmentsPreviewEvents.ClearSendState) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle) - } - } - - private fun createAttachmentsPreviewPresenter( - localMedia: LocalMedia = aLocalMedia( - uri = mockMediaUrl, - ), - room: MatrixRoom = FakeMatrixRoom() - ): AttachmentsPreviewPresenter { - return AttachmentsPreviewPresenter( - attachment = Attachment.Media(localMedia, compressIfPossible = false), - mediaSender = MediaSender(mediaPreProcessor, room) - ) - } -} 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 8da508716c..3936559145 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 @@ -105,6 +105,7 @@ fun TextComposer( modifier: Modifier = Modifier, showTextFormatting: Boolean = false, subcomposing: Boolean = false, + hasAttachments: Boolean = false, ) { val markdown = when (state) { is TextEditorState.Markdown -> state.state.text.value() @@ -142,6 +143,8 @@ fun TextComposer( val placeholder = if (composerMode.inThread) { stringResource(id = CommonStrings.action_reply_in_thread) + } else if (hasAttachments) { + stringResource(id = R.string.rich_text_editor_composer_caption_placeholder) } else { stringResource(id = R.string.rich_text_editor_composer_placeholder) } @@ -190,7 +193,7 @@ fun TextComposer( val canSendMessage = markdown.isNotBlank() val sendButton = @Composable { SendButton( - canSendMessage = canSendMessage, + canSendMessage = canSendMessage || hasAttachments, onClick = onSendClick, composerMode = composerMode, ) @@ -219,7 +222,7 @@ fun TextComposer( } val sendOrRecordButton = when { - enableVoiceMessages && !canSendMessage -> + enableVoiceMessages && !canSendMessage && !hasAttachments -> when (voiceMessageState) { VoiceMessageState.Idle, is VoiceMessageState.Recording -> recordVoiceButton diff --git a/libraries/textcomposer/impl/src/main/res/values/localazy.xml b/libraries/textcomposer/impl/src/main/res/values/localazy.xml index 10289d2c98..bc9a260d23 100644 --- a/libraries/textcomposer/impl/src/main/res/values/localazy.xml +++ b/libraries/textcomposer/impl/src/main/res/values/localazy.xml @@ -5,6 +5,7 @@ "Close formatting options" "Toggle code block" "Messageā€¦" + "Captionā€¦" "Create a link" "Edit link" "Apply bold format"