From 266d48b48bb60e33d3964c29319b24541f5b8b16 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 5 May 2023 19:48:50 +0200 Subject: [PATCH] Introduce Attachments and LocalMedia (WIP) --- features/messages/impl/build.gradle.kts | 3 + .../messages/impl/MessagesFlowNode.kt | 37 ++++-- .../features/messages/impl/MessagesNode.kt | 8 ++ .../features/messages/impl/MessagesView.kt | 13 +- .../messages/impl/attachments/Attachment.kt | 29 +++++ .../preview/AttachmentsPreviewNode.kt | 53 ++++++++ .../preview/AttachmentsPreviewPresenter.kt | 42 +++++++ .../preview/AttachmentsPreviewState.kt | 23 ++++ .../AttachmentsPreviewStateProvider.kt | 36 ++++++ .../preview/AttachmentsPreviewView.kt | 66 ++++++++++ .../media/local/AndroidLocalMediaFactory.kt | 36 ++++++ .../impl/media/local/FakeLocalMediaFactory.kt | 29 +++++ .../LocalMedia.kt} | 27 ++--- .../LocalMediaFactory.kt} | 9 +- .../impl/media/local/LocalMediaView.kt | 113 ++++++++++++++++++ .../impl/media/viewer/MediaViewerNode.kt | 13 +- .../impl/media/viewer/MediaViewerPresenter.kt | 63 ++++++++++ .../impl/media/viewer/MediaViewerState.kt | 6 +- .../media/viewer/MediaViewerStateProvider.kt | 13 +- .../impl/media/viewer/MediaViewerView.kt | 80 ++++--------- .../textcomposer/MessageComposerEvents.kt | 2 + .../textcomposer/MessageComposerPresenter.kt | 39 +++--- .../impl/textcomposer/MessageComposerState.kt | 9 ++ .../MessageComposerStateProvider.kt | 1 + gradle/libs.versions.toml | 3 + 25 files changed, 634 insertions(+), 119 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/{viewer/model/MediaContentUiModel.kt => local/LocalMedia.kt} (54%) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/{viewer/MediaViewerEvents.kt => local/LocalMediaFactory.kt} (74%) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 1b101b40f3..f9f32e6e0e 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -50,6 +50,9 @@ dependencies { implementation(libs.accompanist.flowlayout) implementation(libs.androidx.recyclerview) implementation(libs.jsoup) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.ui) + implementation(libs.accompanist.systemui) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) 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 95a5d4ec9e..24122459a6 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 @@ -26,18 +26,23 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode 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.media.viewer.MediaViewerNode -import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.media.MatrixMediaSource import kotlinx.android.parcel.Parcelize +import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) class MessagesFlowNode @AssistedInject constructor( @@ -57,7 +62,10 @@ class MessagesFlowNode @AssistedInject constructor( object Messages : NavTarget @Parcelize - data class MediaViewer(val mediaContent: MediaContentUiModel) : NavTarget + data class MediaViewer(val title: String, val mediaSource: MatrixMediaSource) : NavTarget + + @Parcelize + data class AttachmentPreview(val attachment: Attachment) : NavTarget } private val callback = plugins().firstOrNull() @@ -73,25 +81,35 @@ class MessagesFlowNode @AssistedInject constructor( override fun onEventClicked(event: TimelineItem.Event) { processEventClicked(event) } + + override fun onPreviewAttachments(attachments: ImmutableList) { + backstack.push(NavTarget.AttachmentPreview(attachments.first())) + } } createNode(buildContext, listOf(callback)) } is NavTarget.MediaViewer -> { - val inputs = MediaViewerNode.Inputs(navTarget.mediaContent) + val inputs = MediaViewerNode.Inputs(navTarget.title, navTarget.mediaSource) createNode(buildContext, listOf(inputs)) } + is NavTarget.AttachmentPreview -> { + val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment) + createNode(buildContext, listOf(inputs)) + } } } private fun processEventClicked(event: TimelineItem.Event) { when (event.content) { is TimelineItemImageContent -> { - val mediaContent = MediaContentUiModel.Image( - body = event.content.body, - url = event.content.mediaRequestData.url, - blurhash = event.content.blurhash - ) - backstack.push(NavTarget.MediaViewer(mediaContent)) + val mediaSource = event.content.mediaSource + val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource) + backstack.push(navTarget) + } + is TimelineItemVideoContent -> { + val mediaSource = event.content.videoSource + val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource) + backstack.push(navTarget) } else -> Unit } @@ -102,6 +120,7 @@ class MessagesFlowNode @AssistedInject constructor( Children( navModel = backstack, modifier = modifier, + transitionHandler = rememberBackstackFader() ) } } 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 fb0804abd0..626b3bd682 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 @@ -25,8 +25,10 @@ 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.timeline.model.TimelineItem import io.element.android.libraries.di.RoomScope +import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) class MessagesNode @AssistedInject constructor( @@ -40,6 +42,7 @@ class MessagesNode @AssistedInject constructor( interface Callback : Plugin { fun onRoomDetailsClicked() fun onEventClicked(event: TimelineItem.Event) + fun onPreviewAttachments(attachments: ImmutableList) } private fun onRoomDetailsClicked() { @@ -50,6 +53,10 @@ class MessagesNode @AssistedInject constructor( callback?.onEventClicked(event) } + private fun onPreviewAttachments(attachments: ImmutableList) { + callback?.onPreviewAttachments(attachments) + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -58,6 +65,7 @@ class MessagesNode @AssistedInject constructor( onBackPressed = this::navigateUp, onRoomDetailsClicked = this::onRoomDetailsClicked, onEventClicked = this::onEventClicked, + onPreviewAttachments = this::onPreviewAttachments, modifier = modifier, ) } 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 8a14507004..b9cace4ded 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 @@ -62,7 +62,9 @@ import androidx.compose.ui.unit.sp 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.textcomposer.AttachmentSourcePicker +import io.element.android.features.messages.impl.textcomposer.AttachmentsState import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents import io.element.android.features.messages.impl.textcomposer.MessageComposerView import io.element.android.features.messages.impl.timeline.TimelineView @@ -80,6 +82,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import timber.log.Timber @@ -89,6 +92,7 @@ fun MessagesView( onBackPressed: () -> Unit, onRoomDetailsClicked: () -> Unit, onEventClicked: (event: TimelineItem.Event) -> Unit, + onPreviewAttachments: (ImmutableList) -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") @@ -105,6 +109,13 @@ fun MessagesView( val bottomSheetState = rememberModalBottomSheetState(initialValue = initialBottomSheetState) val coroutineScope = rememberCoroutineScope() + val attachmentsState = state.composerState.attachmentsState + if (attachmentsState is AttachmentsState.Previewing) { + LaunchedEffect(attachmentsState) { + onPreviewAttachments(attachmentsState.attachments) + } + } + BackHandler(enabled = bottomSheetState.isVisible) { coroutineScope.launch { bottomSheetState.hide() @@ -327,5 +338,5 @@ internal fun MessagesViewDarkPreview(@PreviewParameter(MessagesStateProvider::cl @Composable private fun ContentToPreview(state: MessagesState) { - MessagesView(state, {}, {}, {}) + MessagesView(state, {}, {}, {}, {}) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt new file mode 100644 index 0000000000..53626a5037 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt @@ -0,0 +1,29 @@ +/* + * 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 + +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.media.local.LocalMedia +import kotlinx.parcelize.Parcelize + +@Immutable +sealed interface Attachment : Parcelable { + + @Parcelize + data class Media(val localMedia: LocalMedia) : Attachment +} 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 new file mode 100644 index 0000000000..2c96ece06b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt @@ -0,0 +1,53 @@ +/* + * 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.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) { + val state = presenter.present() + AttachmentsPreviewView( + state = state, + 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 new file mode 100644 index 0000000000..6bce4a3bad --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -0,0 +1,42 @@ +/* + * 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 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 + +class AttachmentsPreviewPresenter @AssistedInject constructor( + @Assisted private val attachment: Attachment, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(attachment: Attachment): AttachmentsPreviewPresenter + } + + @Composable + override fun present(): AttachmentsPreviewState { + + return AttachmentsPreviewState( + attachment = attachment, + ) + } +} 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 new file mode 100644 index 0000000000..8e1b7fca78 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt @@ -0,0 +1,23 @@ +/* + * 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 io.element.android.features.messages.impl.attachments.Attachment + +data class AttachmentsPreviewState( + val attachment: Attachment, +) 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 new file mode 100644 index 0000000000..122a26ab5e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -0,0 +1,36 @@ +/* + * 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.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.media.local.LocalMedia + +open class AttachmentsPreviewStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aAttachmentsPreviewState(), + // Add other states here + ) +} + +fun aAttachmentsPreviewState() = AttachmentsPreviewState( + attachment = Attachment.Media( + localMedia = LocalMedia("".toUri(), mimeType = null) + ) +) 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 new file mode 100644 index 0000000000..6b4449b620 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -0,0 +1,66 @@ +/* + * 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(ExperimentalMaterial3Api::class) + +package io.element.android.features.messages.impl.attachments.preview + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.media.local.LocalMediaView +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Scaffold + +@Composable +fun AttachmentsPreviewView( + state: AttachmentsPreviewState, + modifier: Modifier = Modifier, +) { + Scaffold(modifier) { + Box( + modifier = Modifier, + contentAlignment = Alignment.Center + ) { + when (state.attachment) { + is Attachment.Media -> LocalMediaView(localMedia = state.attachment.localMedia) + } + } + } +} + +@Preview +@Composable +fun AttachmentsPreviewViewLightPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun AttachmentsPreviewViewDarkPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: AttachmentsPreviewState) { + AttachmentsPreviewView( + state = state, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt new file mode 100644 index 0000000000..00136d616e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.media.local + +import android.content.Context +import android.net.Uri +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class AndroidLocalMediaFactory @Inject constructor( + @ApplicationContext private val context: Context +) : LocalMediaFactory { + + override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? { + if (uri == null) return null + val resolvedMimeType = mimeType ?: context.contentResolver.getType(uri) + return LocalMedia(uri, resolvedMimeType) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt new file mode 100644 index 0000000000..7f20492972 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.media.local + +import android.net.Uri + +class FakeLocalMediaFactory() : LocalMediaFactory { + + var mimeType: String? = null + + override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? { + if (uri == null) return null + return LocalMedia(uri, mimeType) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt similarity index 54% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index 0f409364d2..1803bb3fb1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -14,27 +14,14 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.viewer.model +package io.element.android.features.messages.impl.media.local +import android.net.Uri import android.os.Parcelable -import io.element.android.libraries.matrix.ui.media.MediaRequestData import kotlinx.android.parcel.Parcelize -sealed interface MediaContentUiModel : Parcelable { - - @Parcelize - data class Image( - val body: String, - val url: String, - val blurhash: String?, - ) : MediaContentUiModel { - val mediaRequestData = MediaRequestData( - url = url, kind = MediaRequestData.Kind.Content - ) - } - - @Parcelize - data class Video( - val body: String, - ) : MediaContentUiModel -} +@Parcelize +data class LocalMedia( + val uri: Uri, + val mimeType: String?, +) : Parcelable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt similarity index 74% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt index 2f4398d570..08f026c4ac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt @@ -14,9 +14,10 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.viewer +package io.element.android.features.messages.impl.media.local -// TODO Add your events or remove the file completely if no events -sealed interface MediaViewerEvents { - object MyEvent : MediaViewerEvents +import android.net.Uri + +interface LocalMediaFactory { + fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt new file mode 100644 index 0000000000..2cbe08543c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -0,0 +1,113 @@ +/* + * 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.media.local + +import android.annotation.SuppressLint +import android.net.Uri +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.media3.common.MediaItem +import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView +import coil.compose.AsyncImage +import io.element.android.libraries.designsystem.components.ZoomableBox +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent + +@SuppressLint("UnsafeOptInUsageError") +@Composable +fun LocalMediaView( + localMedia: LocalMedia, + modifier: Modifier = Modifier +) { + when { + MimeTypes.isImage(localMedia.mimeType) -> MediaImageView( + uri = localMedia.uri, + modifier = modifier + ) + MimeTypes.isVideo(localMedia.mimeType) -> MediaVideoView( + uri = localMedia.uri, + modifier = modifier + ) + else -> Unit + } +} + +@Composable +private fun MediaImageView( + uri: Uri, + modifier: Modifier = Modifier, +) { + ZoomableBox( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + AsyncImage( + modifier = Modifier + .zoomable() + .fillMaxSize(), + model = uri, + contentDescription = "Image", + contentScale = ContentScale.Fit, + ) + } +} + +@UnstableApi +@Composable +fun MediaVideoView( + uri: Uri, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val exoPlayer = ExoPlayer.Builder(LocalContext.current).build() + val mediaItem = MediaItem.Builder() + .setUri(uri) + .build() + exoPlayer.playWhenReady + exoPlayer.setMediaItem(mediaItem) + exoPlayer.prepare() + + AndroidView( + factory = { + PlayerView(context).apply { + player = exoPlayer + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + } + }, modifier = modifier.fillMaxSize() + ) + + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> exoPlayer.play() + Lifecycle.Event.ON_PAUSE -> exoPlayer.pause() + Lifecycle.Event.ON_DESTROY -> exoPlayer.release() + else -> Unit + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt index 439d5c1ea1..02ec781815 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -24,25 +24,32 @@ 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.features.messages.impl.media.viewer.model.MediaContentUiModel import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.media.MatrixMediaSource @ContributesNode(RoomScope::class) class MediaViewerNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + presenterFactory: MediaViewerPresenter.Factory, ) : Node(buildContext, plugins = plugins) { - data class Inputs(val mediaContent: MediaContentUiModel) : NodeInputs + data class Inputs( + val name: String, + val mediaSource: MatrixMediaSource, + ) : NodeInputs private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create(inputs.name, inputs.mediaSource) + @Composable override fun View(modifier: Modifier) { + val state = presenter.present() MediaViewerView( - state = MediaViewerState(inputs.mediaContent), + state = state, modifier = modifier ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt new file mode 100644 index 0000000000..70408cc580 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -0,0 +1,63 @@ +/* + * 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.media.viewer + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.features.messages.impl.media.local.LocalMediaFactory +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.media.MatrixMediaSource + +class MediaViewerPresenter @AssistedInject constructor( + @Assisted private val name: String, + @Assisted private val mediaSource: MatrixMediaSource, + private val localMediaFactory: LocalMediaFactory, + private val client: MatrixClient, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(name: String, mediaSource: MatrixMediaSource): MediaViewerPresenter + } + + @Composable + override fun present(): MediaViewerState { + val localMedia by produceState>(initialValue = Async.Uninitialized) { + value = Async.Loading(null) + //TODO we are missing some permissions to use this API + client.mediaLoader.loadMediaFile(mediaSource, null) + .onSuccess { + val localMedia = localMediaFactory.createFromUri(uri = it, null) + Async.Success(localMedia) + }.onFailure { + Async.Failure(it, null) + } + } + + return MediaViewerState( + name = name, + downloadedMedia = localMedia, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt index a8d02dea15..6a486b04fe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt @@ -16,8 +16,10 @@ package io.element.android.features.messages.impl.media.viewer -import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel +import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.libraries.architecture.Async data class MediaViewerState( - val mediaContent: MediaContentUiModel + val name: String, + val downloadedMedia: Async ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt index 61cb4d36a1..19bc34535b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -17,22 +17,17 @@ package io.element.android.features.messages.impl.media.viewer import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel +import io.element.android.libraries.architecture.Async open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaViewerState(), + // Add other states here ) } fun aMediaViewerState() = MediaViewerState( - mediaContent = aMediaImage(), + name = "A media", + downloadedMedia = Async.Uninitialized ) - -private fun aMediaImage() = MediaContentUiModel.Image( - body = "a body", - url = "", - blurhash = null, -) - diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index 38377ea05e..f3b42f1cc6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -14,84 +14,50 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.features.messages.impl.media.viewer -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.calculatePan -import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel -import io.element.android.libraries.designsystem.components.ZoomableBox -import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.media.local.LocalMediaView +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Scaffold @Composable fun MediaViewerView( state: MediaViewerState, modifier: Modifier = Modifier, ) { - Box( - modifier = modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - contentAlignment = Alignment.Center, - ) { - when (state.mediaContent) { - is MediaContentUiModel.Image -> MediaImageViewer(state.mediaContent) - is MediaContentUiModel.Video -> MediaVideoViewer(state.mediaContent) + Scaffold(modifier) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + when (state.downloadedMedia) { + is Async.Success -> LocalMediaView(state.downloadedMedia.state) + is Async.Failure -> ErrorDialog( + content = "Error while downloading the media", + ) + else -> CircularProgressIndicator( + strokeWidth = 2.dp, + ) + } } } } -@Composable -private fun MediaImageViewer( - image: MediaContentUiModel.Image, - modifier: Modifier = Modifier, -) { - ZoomableBox( - modifier = modifier.fillMaxSize(), - ) { - BlurHashAsyncImage( - blurHash = image.blurhash, - modifier = Modifier.fillMaxSize().zoomable(), - model = image.mediaRequestData, - contentScale = ContentScale.Fit, - ) - } -} - -@Composable -private fun MediaVideoViewer( - video: MediaContentUiModel.Video, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - - } -} - - @Preview @Composable fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt index b3728d1367..c89a08744a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt @@ -16,8 +16,10 @@ package io.element.android.features.messages.impl.textcomposer +import androidx.compose.runtime.Immutable import io.element.android.libraries.textcomposer.MessageComposerMode +@Immutable sealed interface MessageComposerEvents { object ToggleFullScreenState : MessageComposerEvents data class SendMessage(val message: String) : MessageComposerEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt index 8d495aa269..d6133c5cc6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt @@ -16,6 +16,8 @@ package io.element.android.features.messages.impl.textcomposer +import android.annotation.SuppressLint +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -25,6 +27,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.media3.common.MimeTypes +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.toStableCharSequence @@ -35,9 +40,9 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.mediapickers.PickerProvider import io.element.android.libraries.textcomposer.MessageComposerMode +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @SingleIn(RoomScope::class) @@ -46,27 +51,32 @@ class MessageComposerPresenter @Inject constructor( private val room: MatrixRoom, private val mediaPickerProvider: PickerProvider, private val featureFlagService: FeatureFlagService, + private val localMediaFactory: LocalMediaFactory, ) : Presenter { + @SuppressLint("UnsafeOptInUsageError") @Composable override fun present(): MessageComposerState { val localCoroutineScope = rememberCoroutineScope() - val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri -> - Timber.d("Media picked from $uri") - }) + val attachmentsState = remember { + mutableStateOf(AttachmentsState.None) + } - val filesPicker = mediaPickerProvider.registerFilePicker(onResult = { uri -> - Timber.d("File picked from $uri") - }) + fun handlePickedMedia(uri: Uri?, mimeType: String? = null) { + val localMedia = localMediaFactory.createFromUri(uri, mimeType) + attachmentsState.value = if (localMedia == null) { + AttachmentsState.None + } else { + val mediaAttachment = Attachment.Media(localMedia) + AttachmentsState.Previewing(persistentListOf(mediaAttachment)) + } + } - val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri -> - Timber.d("Photo saved at $uri") - }) - - val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { uri -> - Timber.d("Video saved at $uri") - }) + val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { handlePickedMedia(it) }) + val filesPicker = mediaPickerProvider.registerFilePicker(onResult = { handlePickedMedia(it) }) + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.IMAGE_JPEG) }, deleteAfter = false) + val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.VIDEO_MP4) }, deleteAfter = false) val isFullScreen = rememberSaveable { mutableStateOf(false) @@ -129,6 +139,7 @@ class MessageComposerPresenter @Inject constructor( isFullScreen = isFullScreen.value, mode = composerMode.value, attachmentSourcePicker = attachmentSourcePicker, + attachmentsState = attachmentsState.value, eventSink = ::handleEvents ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt index 7824f1f242..5129792313 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt @@ -17,8 +17,10 @@ package io.element.android.features.messages.impl.textcomposer import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.textcomposer.MessageComposerMode +import kotlinx.collections.immutable.ImmutableList @Immutable data class MessageComposerState( @@ -26,11 +28,18 @@ data class MessageComposerState( val isFullScreen: Boolean, val mode: MessageComposerMode, val attachmentSourcePicker: AttachmentSourcePicker?, + val attachmentsState: AttachmentsState, val eventSink: (MessageComposerEvents) -> Unit ) { val isSendButtonVisible: Boolean = text?.charSequence.isNullOrEmpty().not() } +@Immutable +sealed interface AttachmentsState { + object None : AttachmentsState + data class Previewing(val attachments: ImmutableList) : AttachmentsState +} + sealed interface AttachmentSourcePicker { object AllMedia : AttachmentSourcePicker object Camera : AttachmentSourcePicker diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt index 38a88ee91f..af4dfd111d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt @@ -32,5 +32,6 @@ fun aMessageComposerState() = MessageComposerState( isFullScreen = false, mode = MessageComposerMode.Normal(content = ""), attachmentSourcePicker = null, + attachmentsState = AttachmentsState.None, eventSink = {} ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b4a9186cd5..885e480846 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ recyclerview = "1.3.0" lifecycle = "2.6.1" activity = "1.7.1" startup = "1.1.1" +media3 = "1.0.1" # Compose compose_bom = "2023.04.01" @@ -69,6 +70,8 @@ androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx_splash = "androidx.core:core-splashscreen:1.0.1" androidx_security_crypto = "androidx.security:security-crypto:1.0.0" +androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } +androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "media3" } androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" } androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }