diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 3bd73d5ed2..ea037f11f4 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -18,6 +18,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt index 6b0df92f09..abf451b4b6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt @@ -18,12 +18,10 @@ package io.element.android.features.messages.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.core.RoomId import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -33,6 +31,6 @@ class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint { buildContext: BuildContext, callback: MessagesEntryPoint.Callback ): Node { - return parentNode.createNode(buildContext, listOf(callback)) + return parentNode.createNode(buildContext, listOf(callback)) } } 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 new file mode 100644 index 0000000000..95a5d4ec9e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -0,0 +1,107 @@ +/* + * 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 + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +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 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.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.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.RoomScope +import kotlinx.android.parcel.Parcelize + +@ContributesNode(RoomScope::class) +class MessagesFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Messages, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + object Messages : NavTarget + + @Parcelize + data class MediaViewer(val mediaContent: MediaContentUiModel) : NavTarget + } + + private val callback = plugins().firstOrNull() + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.Messages -> { + val callback = object : MessagesNode.Callback { + override fun onRoomDetailsClicked() { + callback?.onRoomDetailsClicked() + } + + override fun onEventClicked(event: TimelineItem.Event) { + processEventClicked(event) + } + } + createNode(buildContext, listOf(callback)) + } + is NavTarget.MediaViewer -> { + val inputs = MediaViewerNode.Inputs(navTarget.mediaContent) + 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)) + } + else -> Unit + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + ) + } +} 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 9665907eb2..fb0804abd0 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,9 +25,8 @@ 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.api.MessagesEntryPoint +import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(RoomScope::class) class MessagesNode @AssistedInject constructor( @@ -36,12 +35,21 @@ class MessagesNode @AssistedInject constructor( private val presenter: MessagesPresenter, ) : Node(buildContext, plugins = plugins) { - private val callback = plugins().firstOrNull() + private val callback = plugins().firstOrNull() + + interface Callback : Plugin { + fun onRoomDetailsClicked() + fun onEventClicked(event: TimelineItem.Event) + } private fun onRoomDetailsClicked() { callback?.onRoomDetailsClicked() } + private fun onEventClicked(event: TimelineItem.Event) { + callback?.onEventClicked(event) + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -49,7 +57,8 @@ class MessagesNode @AssistedInject constructor( state = state, onBackPressed = this::navigateUp, onRoomDetailsClicked = this::onRoomDetailsClicked, - modifier = modifier + onEventClicked = this::onEventClicked, + 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 132d139174..7dc66d076e 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 @@ -60,6 +60,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc import io.element.android.features.messages.impl.textcomposer.MessageComposerView import io.element.android.features.messages.impl.timeline.TimelineView import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -70,16 +71,16 @@ 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 io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import kotlinx.coroutines.launch import timber.log.Timber @Composable fun MessagesView( state: MessagesState, + onBackPressed: () -> Unit, + onRoomDetailsClicked: () -> Unit, + onEventClicked: (event: TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, - onBackPressed: () -> Unit = {}, - onRoomDetailsClicked: () -> Unit = {}, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") val itemActionsBottomSheetState = rememberModalBottomSheetState( @@ -93,6 +94,7 @@ fun MessagesView( fun onMessageClicked(event: TimelineItem.Event) { Timber.v("OnMessageClicked= ${event.id}") + onEventClicked(event) } fun onMessageLongClicked(event: TimelineItem.Event) { @@ -228,5 +230,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/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt new file mode 100644 index 0000000000..2f4398d570 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt @@ -0,0 +1,22 @@ +/* + * 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 + +// TODO Add your events or remove the file completely if no events +sealed interface MediaViewerEvents { + object MyEvent : MediaViewerEvents +} 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 new file mode 100644 index 0000000000..439d5c1ea1 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -0,0 +1,49 @@ +/* + * 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.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.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 + +@ContributesNode(RoomScope::class) +class MediaViewerNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : Node(buildContext, plugins = plugins) { + + data class Inputs(val mediaContent: MediaContentUiModel) : NodeInputs + + private val inputs: Inputs = inputs() + + @Composable + override fun View(modifier: Modifier) { + MediaViewerView( + state = MediaViewerState(inputs.mediaContent), + modifier = modifier + ) + } +} 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 new file mode 100644 index 0000000000..a8d02dea15 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.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.media.viewer + +import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel + +data class MediaViewerState( + val mediaContent: MediaContentUiModel +) 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 new file mode 100644 index 0000000000..61cb4d36a1 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -0,0 +1,38 @@ +/* + * 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.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel + +open class MediaViewerStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aMediaViewerState(), + ) +} + +fun aMediaViewerState() = MediaViewerState( + mediaContent = aMediaImage(), +) + +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 new file mode 100644 index 0000000000..0ce7c09ea5 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -0,0 +1,97 @@ +/* + * 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.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +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.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.blurhash.BlurHashAsyncImage +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@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) + } + } +} + +@Composable +private fun MediaImageViewer( + image: MediaContentUiModel.Image, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + BlurHashAsyncImage( + blurHash = image.blurhash, + model = image.mediaRequestData, + contentScale = ContentScale.Crop, + ) + } +} + +@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) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun MediaViewerViewDarkPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: MediaViewerState) { + MediaViewerView( + state = state, + ) +} 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/viewer/model/MediaContentUiModel.kt new file mode 100644 index 0000000000..0f409364d2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt @@ -0,0 +1,40 @@ +/* + * 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.model + +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 +}