Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/src/main/kotlin/io/element/android/x/di/AppGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import dev.zacsweers.metro.Multibinds
import dev.zacsweers.metro.Provides
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.mediaupload.api.VideoMetadataExtractor
import io.element.android.libraries.mediaupload.impl.DefaultVideoMetadataExtractorFactory
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import kotlin.reflect.KClass

Expand All @@ -27,6 +29,11 @@ interface AppGraph : NodeFactoriesBindings {
val workerProviders:
Map<KClass<out ListenableWorker>, MetroWorkerFactory.WorkerInstanceFactory<*>>

@Provides
fun providesVideoMetadataExtractorFactory(
@ApplicationContext context: Context
): VideoMetadataExtractor.Factory = DefaultVideoMetadataExtractorFactory(context)

@DependencyGraph.Factory
interface Factory {
fun create(
Expand Down
22 changes: 22 additions & 0 deletions features/mediapreview/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

plugins {
id("io.element.android-library")
id("kotlin-parcelize")
}

android {
namespace = "io.element.android.features.mediapreview.api"
}

dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.libraries.preferences.api)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.mediapreview.api

import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset

interface MediaPreviewEntryPoint : FeatureEntryPoint {
data class Params(
val localMedia: LocalMedia,
val config: MediaPreviewConfig = MediaPreviewConfig(),
)

fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: Params,
callback: Callback,
): Node

interface Callback : Plugin {
fun onSend(
caption: String?,
optimizeImage: Boolean,
videoPreset: VideoCompressionPreset?,
onComplete: () -> Unit,
)
fun onCancel()
}
}

data class MediaPreviewConfig(
val initialCaption: String? = null,
val showProgressDialog: Boolean = false,
val sendMode: SendMode = SendMode.DIRECT,
val timelineMode: io.element.android.libraries.matrix.api.timeline.Timeline.Mode = io.element.android.libraries.matrix.api.timeline.Timeline.Mode.Live,
val inReplyToEventId: io.element.android.libraries.matrix.api.core.EventId? = null,
val joinedRoom: io.element.android.libraries.matrix.api.room.JoinedRoom? = null,
)

enum class SendMode {
DIRECT,
PREPROCESS,
}
55 changes: 55 additions & 0 deletions features/mediapreview/impl/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies

/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}

android {
namespace = "io.element.android.features.mediapreview.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}

setupDependencyInjection()

dependencies {
api(projects.features.mediapreview.api)
implementation(projects.appconfig)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.mediaupload.impl)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.libraries.textcomposer.impl)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.di)
implementation(projects.libraries.uiUtils)
implementation(projects.libraries.featureflag.api)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.foundation.layout)
implementation(libs.timber)

testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.mediaviewer.test)
testImplementation(projects.libraries.featureflag.test)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.mediapreview.impl

import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.mediapreview.api.MediaPreviewEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope

@ContributesBinding(SessionScope::class)
class DefaultMediaPreviewEntryPoint : MediaPreviewEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: MediaPreviewEntryPoint.Params,
callback: MediaPreviewEntryPoint.Callback,
): Node {
return parentNode.createNode<MediaPreviewNode>(
buildContext = buildContext,
plugins = listOf(
MediaPreviewNode.Inputs(
localMedia = params.localMedia,
config = params.config,
onSend = { caption, optimizeImage, videoPreset, onComplete ->
callback.onSend(caption, optimizeImage, videoPreset, onComplete)
},
onCancel = { callback.onCancel() },
),
callback,
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.mediapreview.impl

import io.element.android.libraries.preferences.api.store.VideoCompressionPreset

sealed interface MediaPreviewEvents {
data object Send : MediaPreviewEvents
data object Cancel : MediaPreviewEvents
data object Retry : MediaPreviewEvents
data object ClearError : MediaPreviewEvents
data class ToggleImageOptimization(val enabled: Boolean) : MediaPreviewEvents
data class SelectVideoQuality(val preset: VideoCompressionPreset) : MediaPreviewEvents
data object ShowVideoQualityDialog : MediaPreviewEvents
data object DismissVideoQualityDialog : MediaPreviewEvents
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.mediapreview.impl

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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.mediapreview.api.MediaPreviewConfig
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset

@ContributesNode(SessionScope::class)
@AssistedInject
class MediaPreviewNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: MediaPreviewPresenter.Factory,
private val localMediaRenderer: LocalMediaRenderer,
) : Node(buildContext, plugins = plugins) {
data class Inputs(
val localMedia: LocalMedia,
val config: MediaPreviewConfig,
val onSend: (String?, Boolean, VideoCompressionPreset?, () -> Unit) -> Unit,
val onCancel: () -> Unit,
) : NodeInputs

private val inputs = inputs<Inputs>()

private val presenter = presenterFactory.create(
localMedia = inputs.localMedia,
config = inputs.config,
onSendListener = object : MediaPreviewPresenter.OnSendListener {
override fun onSend(
caption: String?,
optimizeImage: Boolean,
videoPreset: VideoCompressionPreset?,
onComplete: () -> Unit,
) {
inputs.onSend(caption, optimizeImage, videoPreset, onComplete)
}
},
onCancelListener = object : MediaPreviewPresenter.OnCancelListener {
override fun onCancel() {
inputs.onCancel()
}
},
)

@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
MediaPreviewView(
state = state,
localMediaRenderer = localMediaRenderer,
modifier = modifier,
)
}
}
Loading