Introduce ShareDataSender so that SharePresenter is not session scoped

Instead of creating a SharePresenter from the graph of another session, the
SharePresenter now gets the client of the selected session from the
MatrixClientProvider and uses an AppScope ShareDataSender to send the data.

To be able to send media from AppScope, MediaSenderRoomFactory is now bound in
AppScope and takes the MatrixClient as a parameter, and a new
MediaOptimizationConfigProviderFactory provides the media optimization config
of any session.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benoit Marty
2026-10-07 10:34:20 +02:00
parent ed2a553736
commit 72e3a7d92b
15 changed files with 436 additions and 264 deletions
@@ -0,0 +1,101 @@
/*
* 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.share.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.share.api.OnSharedData
import io.element.android.features.share.api.ShareIntentData
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProviderFactory
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlin.coroutines.cancellation.CancellationException
@ContributesBinding(AppScope::class)
class DefaultShareDataSender(
private val mediaSenderRoomFactory: MediaSenderRoomFactory,
private val mediaOptimizationConfigProviderFactory: MediaOptimizationConfigProviderFactory,
private val activeRoomsHolder: ActiveRoomsHolder,
private val onSharedData: OnSharedData,
) : ShareDataSender {
override suspend fun send(
client: MatrixClient,
shareIntentData: ShareIntentData,
roomIds: List<RoomId>,
): Result<Unit> = runCatchingExceptions {
val result = when (shareIntentData) {
is ShareIntentData.PlainText -> {
roomIds
.map { roomId ->
client.findJoinedRoom(roomId)?.liveTimeline?.sendMessage(
body = shareIntentData.content,
htmlBody = null,
intentionalMentions = emptyList(),
)?.isSuccess.orFalse()
}
.all { it }
}
is ShareIntentData.Uris -> {
val filesToShare = shareIntentData.uris
if (filesToShare.isEmpty()) {
false
} else {
val mediaOptimizationConfigProvider = mediaOptimizationConfigProviderFactory.create(client)
roomIds
.map { roomId ->
val room = client.findJoinedRoom(roomId) ?: return@map false
val mediaSender = mediaSenderRoomFactory.create(client = client, room = room)
filesToShare
.map { fileToShare ->
val result = mediaSender.sendMedia(
caption = shareIntentData.text,
uri = fileToShare.uri,
mimeType = fileToShare.mimeType,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
)
// If the coroutine was cancelled, destroy the room and rethrow the exception
val cancellationException = result.exceptionOrNull() as? CancellationException
if (cancellationException != null) {
if (activeRoomsHolder.getActiveRoomMatching(client.sessionId, roomId) == null) {
room.destroy()
}
throw cancellationException
}
result.isSuccess
}
.all { isSuccess -> isSuccess }
.also {
if (activeRoomsHolder.getActiveRoomMatching(client.sessionId, roomId) == null) {
room.destroy()
}
}
}
.all { it }
}
}
}
// Handle post-processing of shared data
onSharedData(shareIntentData)
if (!result) {
error("Failed to handle incoming share intent")
}
}
private suspend fun MatrixClient.findJoinedRoom(roomId: RoomId): JoinedRoom? {
return activeRoomsHolder.getActiveRoom(sessionId)
?.takeIf { it.roomId == roomId }
?: getJoinedRoom(roomId)
}
}
@@ -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.
*/
package io.element.android.features.share.impl
import io.element.android.features.share.api.ShareIntentData
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Sends the shared data to some rooms of a session.
*/
interface ShareDataSender {
/**
* Send [shareIntentData] to the rooms with [roomIds], using the session of [client].
*/
suspend fun send(client: MatrixClient, shareIntentData: ShareIntentData, roomIds: List<RoomId>): Result<Unit>
}
@@ -11,12 +11,7 @@ package io.element.android.features.share.impl
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
@@ -28,20 +23,15 @@ import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.share.api.ShareEntryPoint import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.impl.di.ShareBindings
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.SessionGraphFactory
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@AssistedInject @AssistedInject
@@ -51,8 +41,6 @@ class ShareNode(
presenterFactory: SharePresenter.Factory, presenterFactory: SharePresenter.Factory,
private val roomSelectEntryPoint: RoomSelectEntryPoint, private val roomSelectEntryPoint: RoomSelectEntryPoint,
private val sessionId: SessionId, private val sessionId: SessionId,
private val matrixClientProvider: MatrixClientProvider,
private val sessionGraphFactory: SessionGraphFactory,
) : ParentNode<ShareNode.NavTarget>( ) : ParentNode<ShareNode.NavTarget>(
navModel = PermanentNavModel( navModel = PermanentNavModel(
navTargets = setOf(NavTarget), navTargets = setOf(NavTarget),
@@ -66,53 +54,14 @@ class ShareNode(
data class Inputs(val shareIntentData: ShareIntentData) : NodeInputs data class Inputs(val shareIntentData: ShareIntentData) : NodeInputs
/**
* The presenter used to share the data, and the session it belongs to.
*/
private data class ActiveShare(
val sessionId: SessionId,
val presenter: SharePresenter,
)
private val inputs = inputs<Inputs>() private val inputs = inputs<Inputs>()
private val presenter = presenterFactory.create(inputs.shareIntentData, sessionId)
private val callback: ShareEntryPoint.Callback = callback() private val callback: ShareEntryPoint.Callback = callback()
private val currentSessionShare = ActiveShare(
sessionId = sessionId,
presenter = presenterFactory.create(inputs.shareIntentData),
)
private var activeShare by mutableStateOf(currentSessionShare)
private fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) {
if (sessionId == currentSessionShare.sessionId) {
activeShare = currentSessionShare
currentSessionShare.presenter.onRoomSelected(roomIds)
} else if (sessionId == activeShare.sessionId) {
// Retry with the same other session
activeShare.presenter.onRoomSelected(roomIds)
} else {
lifecycleScope.launch {
// Share using the graph of the selected session, the current session will be changed once the share is done
matrixClientProvider.getOrRestore(sessionId)
.onSuccess { client ->
val presenterFactory = (sessionGraphFactory.create(client) as ShareBindings).sharePresenterFactory()
activeShare = ActiveShare(
sessionId = sessionId,
presenter = presenterFactory.create(inputs.shareIntentData),
).also {
it.presenter.onRoomSelected(roomIds)
}
}
.onFailure {
Timber.e(it, "Failed to get the client for session $sessionId")
}
}
}
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
val callback = object : RoomSelectEntryPoint.Callback { val callback = object : RoomSelectEntryPoint.Callback {
override fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) { override fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) {
this@ShareNode.onRoomSelected(sessionId, roomIds) presenter.onRoomSelected(sessionId, roomIds)
} }
override fun onCancel() { override fun onCancel() {
@@ -140,14 +89,11 @@ class ShareNode(
navModel = navModel, navModel = navModel,
) )
val currentShare = activeShare val state = presenter.present()
key(currentShare) { ShareView(
val state = currentShare.presenter.present() state = state,
ShareView( onShareSuccess = { roomIds -> callback.onDone(state.sessionId, roomIds) },
state = state, )
onShareSuccess = { roomIds -> callback.onDone(currentShare.sessionId, roomIds) },
)
}
} }
} }
} }
@@ -10,47 +10,54 @@ package io.element.android.features.share.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.features.share.api.OnSharedData
import io.element.android.features.share.api.ShareIntentData import io.element.android.features.share.api.ShareIntentData
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.coroutines.cancellation.CancellationException
@AssistedInject @AssistedInject
class SharePresenter( class SharePresenter(
@Assisted private val shareIntentData: ShareIntentData, @Assisted private val shareIntentData: ShareIntentData,
@SessionCoroutineScope @Assisted initialSessionId: SessionId,
private val sessionCoroutineScope: CoroutineScope, @AppCoroutineScope
private val matrixClient: MatrixClient, private val appCoroutineScope: CoroutineScope,
private val mediaSenderRoomFactory: MediaSenderRoomFactory, private val matrixClientProvider: MatrixClientProvider,
private val activeRoomsHolder: ActiveRoomsHolder, private val shareDataSender: ShareDataSender,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
private val onSharedData: OnSharedData,
) : Presenter<ShareState> { ) : Presenter<ShareState> {
@AssistedFactory @AssistedFactory
fun interface Factory { fun interface Factory {
fun create(shareIntentData: ShareIntentData): SharePresenter fun create(shareIntentData: ShareIntentData, initialSessionId: SessionId): SharePresenter
} }
private val shareActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized) private val shareActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized)
fun onRoomSelected(roomIds: List<RoomId>) { /**
sessionCoroutineScope.share(shareIntentData, roomIds) * The session the data is shared with, it can be updated if the user selects rooms from another session.
*/
private var targetSessionId by mutableStateOf(initialSessionId)
fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) {
if (shareActionState.value.isLoading()) return
targetSessionId = sessionId
appCoroutineScope.launch {
suspend {
val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
shareDataSender.send(client, shareIntentData, roomIds).getOrThrow()
roomIds
}.runCatchingUpdatingState(shareActionState)
}
} }
@Composable @Composable
@@ -62,80 +69,9 @@ class SharePresenter(
} }
return ShareState( return ShareState(
sessionId = targetSessionId,
shareAction = shareActionState.value, shareAction = shareActionState.value,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
private suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? {
return activeRoomsHolder.getActiveRoom(matrixClient.sessionId)
?.takeIf { it.roomId == roomId }
?: matrixClient.getJoinedRoom(roomId)
}
private fun CoroutineScope.share(
shareIntentData: ShareIntentData,
roomIds: List<RoomId>,
) = launch {
suspend {
val result = when (shareIntentData) {
is ShareIntentData.PlainText -> {
roomIds
.map { roomId ->
getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
body = shareIntentData.content,
htmlBody = null,
intentionalMentions = emptyList(),
)?.isSuccess.orFalse()
}
.all { it }
}
is ShareIntentData.Uris -> {
val filesToShare = shareIntentData.uris
if (filesToShare.isEmpty()) {
false
} else {
roomIds
.map { roomId ->
val room = getJoinedRoom(roomId) ?: return@map false
val mediaSender = mediaSenderRoomFactory.create(room = room)
filesToShare
.map { fileToShare ->
val result = mediaSender.sendMedia(
caption = shareIntentData.text,
uri = fileToShare.uri,
mimeType = fileToShare.mimeType,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
)
// If the coroutine was cancelled, destroy the room and rethrow the exception
val cancellationException = result.exceptionOrNull() as? CancellationException
if (cancellationException != null) {
if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) {
room.destroy()
}
throw cancellationException
}
result.isSuccess
}
.all { isSuccess -> isSuccess }
.also {
if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) {
room.destroy()
}
}
}
.all { it }
}
}
}
// Handle post-processing of shared data
onSharedData(shareIntentData)
if (!result) {
error("Failed to handle incoming share intent")
}
roomIds
}.runCatchingUpdatingState(shareActionState)
}
} }
@@ -10,8 +10,10 @@ package io.element.android.features.share.impl
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
data class ShareState( data class ShareState(
val sessionId: SessionId,
val shareAction: AsyncAction<List<RoomId>>, val shareAction: AsyncAction<List<RoomId>>,
val eventSink: (ShareEvent) -> Unit val eventSink: (ShareEvent) -> Unit
) )
@@ -11,6 +11,7 @@ package io.element.android.features.share.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
open class ShareStatePreviewParam : PreviewParameterProvider<ShareState> { open class ShareStatePreviewParam : PreviewParameterProvider<ShareState> {
override val values: Sequence<ShareState> override val values: Sequence<ShareState>
@@ -31,9 +32,11 @@ open class ShareStatePreviewParam : PreviewParameterProvider<ShareState> {
} }
fun aShareState( fun aShareState(
sessionId: SessionId = SessionId("@alice:server.org"),
shareAction: AsyncAction<List<RoomId>> = AsyncAction.Uninitialized, shareAction: AsyncAction<List<RoomId>> = AsyncAction.Uninitialized,
eventSink: (ShareEvent) -> Unit = {} eventSink: (ShareEvent) -> Unit = {}
) = ShareState( ) = ShareState(
sessionId = sessionId,
shareAction = shareAction, shareAction = shareAction,
eventSink = eventSink eventSink = eventSink
) )
@@ -1,20 +0,0 @@
/*
* 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.share.impl.di
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.share.impl.SharePresenter
import io.element.android.libraries.di.SessionScope
/**
* Used to get a [SharePresenter.Factory] from the graph of a session which is not the current one.
*/
@ContributesTo(SessionScope::class)
interface ShareBindings {
fun sharePresenterFactory(): SharePresenter.Factory
}
@@ -0,0 +1,119 @@
/*
* 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.share.impl
import android.net.Uri
import com.google.common.truth.Truth.assertThat
import io.element.android.features.share.api.OnSharedData
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.api.UriToShare
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProviderFactory
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.test.FakeMediaSender
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.robolectric.RobolectricTest
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultShareDataSenderTest : RobolectricTest() {
@Test
fun `send text ok`() = runTest {
val joinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline().apply {
sendMessageLambda = { _, _, _, _, _ -> Result.success(Unit) }
},
)
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, joinedRoom)
}
val onSharedData = lambdaRecorder<ShareIntentData, Unit> { }
val sender = createDefaultShareDataSender(
onSharedData = { onSharedData(it) },
)
val result = sender.send(matrixClient, ShareIntentData.PlainText(A_MESSAGE), listOf(A_ROOM_ID))
assertThat(result.isSuccess).isTrue()
onSharedData.assertions().isCalledOnce()
}
@Test
fun `send text, room not found`() = runTest {
val onSharedData = lambdaRecorder<ShareIntentData, Unit> { }
val sender = createDefaultShareDataSender(
onSharedData = { onSharedData(it) },
)
val result = sender.send(FakeMatrixClient(), ShareIntentData.PlainText(A_MESSAGE), listOf(A_ROOM_ID))
assertThat(result.isFailure).isTrue()
onSharedData.assertions().isCalledOnce()
}
@Test
fun `send media ok`() = runTest {
val sendMediaResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val joinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline(),
)
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, joinedRoom)
}
val mediaSender = FakeMediaSender(
sendMediaResult = sendMediaResult,
)
val sender = createDefaultShareDataSender(
mediaSenderRoomFactory = { _, _ -> mediaSender },
)
val result = sender.send(
client = matrixClient,
shareIntentData = ShareIntentData.Uris(
text = A_MESSAGE,
listOf(
UriToShare(
uri = Uri.parse("content://image.jpg"),
mimeType = MimeTypes.Jpeg,
)
)
),
roomIds = listOf(A_ROOM_ID),
)
assertThat(result.isSuccess).isTrue()
sendMediaResult.assertions().isCalledOnce()
}
@Test
fun `send media with no uris is a failure`() = runTest {
val sender = createDefaultShareDataSender()
val result = sender.send(
client = FakeMatrixClient(),
shareIntentData = ShareIntentData.Uris(text = A_MESSAGE, emptyList()),
roomIds = listOf(A_ROOM_ID),
)
assertThat(result.isFailure).isTrue()
}
}
private fun createDefaultShareDataSender(
mediaSenderRoomFactory: MediaSenderRoomFactory = MediaSenderRoomFactory { _, _ -> FakeMediaSender() },
mediaOptimizationConfigProviderFactory: MediaOptimizationConfigProviderFactory = MediaOptimizationConfigProviderFactory {
FakeMediaOptimizationConfigProvider()
},
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
onSharedData: OnSharedData = OnSharedData {},
) = DefaultShareDataSender(
mediaSenderRoomFactory = mediaSenderRoomFactory,
mediaOptimizationConfigProviderFactory = mediaOptimizationConfigProviderFactory,
activeRoomsHolder = activeRoomsHolder,
onSharedData = onSharedData,
)
@@ -14,13 +14,10 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.share.api.ShareEntryPoint import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData import io.element.android.features.share.api.ShareIntentData
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.SessionGraphFactory
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
@@ -42,13 +39,9 @@ class DefaultShareEntryPointTest {
ShareNode( ShareNode(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
presenterFactory = { createSharePresenter() }, presenterFactory = { _, _ -> createSharePresenter() },
roomSelectEntryPoint = FakeRoomSelectEntryPoint(), roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
matrixClientProvider = FakeMatrixClientProvider(),
sessionGraphFactory = object : SessionGraphFactory {
override fun create(client: MatrixClient): Any = lambdaError()
},
) )
} }
val callback = object : ShareEntryPoint.Callback { val callback = object : ShareEntryPoint.Callback {
@@ -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.share.impl
import io.element.android.features.share.api.ShareIntentData
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.tests.testutils.lambda.lambdaError
class FakeShareDataSender(
private val sendResult: (MatrixClient, ShareIntentData, List<RoomId>) -> Result<Unit> = { _, _, _ -> lambdaError() },
) : ShareDataSender {
override suspend fun send(client: MatrixClient, shareIntentData: ShareIntentData, roomIds: List<RoomId>): Result<Unit> {
return sendResult(client, shareIntentData, roomIds)
}
}
@@ -8,37 +8,32 @@
package io.element.android.features.share.impl package io.element.android.features.share.impl
import android.net.Uri
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.share.api.OnSharedData
import io.element.android.features.share.api.ShareIntentData import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.api.UriToShare
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.test.FakeMediaSender
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
class SharePresenterTest : RobolectricTest() { class SharePresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@@ -49,22 +44,25 @@ class SharePresenterTest : RobolectricTest() {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.sessionId).isEqualTo(A_SESSION_ID)
assertThat(initialState.shareAction.isUninitialized()).isTrue() assertThat(initialState.shareAction.isUninitialized()).isTrue()
} }
} }
@Test @Test
fun `present - on room selected error then clear error`() = runTest { fun `present - on room selected error then clear error`() = runTest {
val presenter = createSharePresenter() val presenter = createSharePresenter(
shareDataSender = FakeShareDataSender { _, _, _ -> Result.failure(AN_EXCEPTION) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.shareAction.isUninitialized()).isTrue() assertThat(initialState.shareAction.isUninitialized()).isTrue()
presenter.onRoomSelected(listOf(A_ROOM_ID)) presenter.onRoomSelected(A_SESSION_ID, listOf(A_ROOM_ID))
assertThat(awaitItem().shareAction.isLoading()).isTrue() assertThat(awaitItem().shareAction.isLoading()).isTrue()
val failure = awaitItem() val failure = awaitItem()
assertThat(failure.shareAction.isFailure()).isTrue() assertThat(failure.shareAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
failure.eventSink.invoke(ShareEvent.ClearError) failure.eventSink.invoke(ShareEvent.ClearError)
assertThat(awaitItem().shareAction.isUninitialized()).isTrue() assertThat(awaitItem().shareAction.isUninitialized()).isTrue()
} }
@@ -72,113 +70,115 @@ class SharePresenterTest : RobolectricTest() {
@Test @Test
fun `present - on room selected ok`() = runTest { fun `present - on room selected ok`() = runTest {
val joinedRoom = FakeJoinedRoom( val client = FakeMatrixClient(sessionId = A_SESSION_ID)
liveTimeline = FakeTimeline().apply { val shareIntentData = ShareIntentData.PlainText(A_MESSAGE)
sendMessageLambda = { _, _, _, _, _ -> Result.success(Unit) } val sendResult = lambdaRecorder<MatrixClient, ShareIntentData, List<RoomId>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
},
)
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, joinedRoom)
}
val presenter = createSharePresenter( val presenter = createSharePresenter(
matrixClient = matrixClient, shareIntentData = shareIntentData,
shareIntentData = ShareIntentData.PlainText(A_MESSAGE), matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
shareDataSender = FakeShareDataSender(sendResult),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.shareAction.isUninitialized()).isTrue() assertThat(initialState.shareAction.isUninitialized()).isTrue()
presenter.onRoomSelected(listOf(A_ROOM_ID)) presenter.onRoomSelected(A_SESSION_ID, listOf(A_ROOM_ID))
assertThat(awaitItem().shareAction.isLoading()).isTrue() assertThat(awaitItem().shareAction.isLoading()).isTrue()
val success = awaitItem() val success = awaitItem()
assertThat(success.shareAction.isSuccess()).isTrue() assertThat(success.sessionId).isEqualTo(A_SESSION_ID)
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID))) assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
sendResult.assertions().isCalledOnce().with(value(client), value(shareIntentData), value(listOf(A_ROOM_ID)))
} }
} }
@Test @Test
fun `present - send text ok`() = runTest { fun `present - on room selected from another session ok`() = runTest {
val joinedRoom = FakeJoinedRoom( val getClient = lambdaRecorder<SessionId, Result<MatrixClient>> { sessionId -> Result.success(FakeMatrixClient(sessionId = sessionId)) }
liveTimeline = FakeTimeline().apply { val sendResult = lambdaRecorder<MatrixClient, ShareIntentData, List<RoomId>, Result<Unit>> { client, _, _ ->
sendMessageLambda = { _, _, _, _, _ -> Result.success(Unit) } assertThat(client.sessionId).isEqualTo(A_SESSION_ID_2)
}, Result.success(Unit)
)
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, joinedRoom)
} }
val presenter = createSharePresenter( val presenter = createSharePresenter(
matrixClient = matrixClient, matrixClientProvider = FakeMatrixClientProvider { getClient(it) },
shareIntentData = ShareIntentData.PlainText(A_MESSAGE), shareDataSender = FakeShareDataSender(sendResult),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.shareAction.isUninitialized()).isTrue() assertThat(initialState.sessionId).isEqualTo(A_SESSION_ID)
presenter.onRoomSelected(listOf(A_ROOM_ID)) presenter.onRoomSelected(A_SESSION_ID_2, listOf(A_ROOM_ID))
val sessionChanged = awaitItem()
assertThat(sessionChanged.sessionId).isEqualTo(A_SESSION_ID_2)
assertThat(sessionChanged.shareAction.isUninitialized()).isTrue()
assertThat(awaitItem().shareAction.isLoading()).isTrue() assertThat(awaitItem().shareAction.isLoading()).isTrue()
val success = awaitItem() val success = awaitItem()
assertThat(success.shareAction.isSuccess()).isTrue() assertThat(success.sessionId).isEqualTo(A_SESSION_ID_2)
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID))) assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
getClient.assertions().isCalledOnce().with(value(A_SESSION_ID_2))
sendResult.assertions().isCalledOnce()
} }
} }
@Test @Test
fun `present - send media ok`() = runTest { fun `present - on room selected, failure to get the client`() = runTest {
val sendMediaResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) } val sendResult = lambdaRecorder<MatrixClient, ShareIntentData, List<RoomId>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
val joinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline(),
)
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, joinedRoom)
}
val mediaSender = FakeMediaSender(
sendMediaResult = sendMediaResult,
)
val presenter = createSharePresenter( val presenter = createSharePresenter(
matrixClient = matrixClient, matrixClientProvider = FakeMatrixClientProvider { Result.failure(AN_EXCEPTION) },
shareIntentData = ShareIntentData.Uris( shareDataSender = FakeShareDataSender(sendResult),
text = A_MESSAGE,
listOf(
UriToShare(
uri = Uri.parse("content://image.jpg"),
mimeType = MimeTypes.Jpeg,
)
)
),
mediaSenderRoomFactory = MediaSenderRoomFactory { mediaSender },
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() skipItems(1)
assertThat(initialState.shareAction.isUninitialized()).isTrue() presenter.onRoomSelected(A_SESSION_ID_2, listOf(A_ROOM_ID))
presenter.onRoomSelected(listOf(A_ROOM_ID)) assertThat(awaitItem().sessionId).isEqualTo(A_SESSION_ID_2)
assertThat(awaitItem().shareAction.isLoading()).isTrue() assertThat(awaitItem().shareAction.isLoading()).isTrue()
assertThat(awaitItem().shareAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
sendResult.assertions().isNeverCalled()
}
}
@Test
fun `present - on room selected while sharing is ignored`() = runTest {
val completable = CompletableDeferred<Result<Unit>>()
val sendResult = lambdaRecorder<MatrixClient, ShareIntentData, List<RoomId>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
val presenter = createSharePresenter(
shareDataSender = object : ShareDataSender {
override suspend fun send(client: MatrixClient, shareIntentData: ShareIntentData, roomIds: List<RoomId>): Result<Unit> {
sendResult(client, shareIntentData, roomIds)
return completable.await()
}
},
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
presenter.onRoomSelected(A_SESSION_ID, listOf(A_ROOM_ID))
assertThat(awaitItem().shareAction.isLoading()).isTrue()
presenter.onRoomSelected(A_SESSION_ID_2, listOf(A_ROOM_ID))
completable.complete(Result.success(Unit))
val success = awaitItem() val success = awaitItem()
assertThat(success.shareAction.isSuccess()).isTrue() assertThat(success.sessionId).isEqualTo(A_SESSION_ID)
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID))) assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
sendMediaResult.assertions().isCalledOnce() sendResult.assertions().isCalledOnce()
} }
} }
} }
internal fun TestScope.createSharePresenter( internal fun TestScope.createSharePresenter(
shareIntentData: ShareIntentData = ShareIntentData.PlainText(A_MESSAGE), shareIntentData: ShareIntentData = ShareIntentData.PlainText(A_MESSAGE),
matrixClient: MatrixClient = FakeMatrixClient(), initialSessionId: SessionId = A_SESSION_ID,
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(), matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
mediaSenderRoomFactory: MediaSenderRoomFactory = MediaSenderRoomFactory { FakeMediaSender() }, shareDataSender: ShareDataSender = FakeShareDataSender(),
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
onSharedData: OnSharedData = OnSharedData {},
): SharePresenter { ): SharePresenter {
return SharePresenter( return SharePresenter(
shareIntentData = shareIntentData, shareIntentData = shareIntentData,
sessionCoroutineScope = this, initialSessionId = initialSessionId,
matrixClient = matrixClient, appCoroutineScope = this,
activeRoomsHolder = activeRoomsHolder, matrixClientProvider = matrixClientProvider,
mediaSenderRoomFactory = mediaSenderRoomFactory, shareDataSender = shareDataSender,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
onSharedData = onSharedData,
) )
} }
@@ -8,6 +8,8 @@
package io.element.android.libraries.mediaupload.api package io.element.android.libraries.mediaupload.api
import io.element.android.libraries.matrix.api.MatrixClient
/** /**
* Resolves how aggressively media should be compressed, combining the user's own preferences with the app defaults. * Resolves how aggressively media should be compressed, combining the user's own preferences with the app defaults.
*/ */
@@ -15,3 +17,15 @@ fun interface MediaOptimizationConfigProvider {
/** Returns the configuration to pass to the pre-processing and sending calls. */ /** Returns the configuration to pass to the pre-processing and sending calls. */
suspend fun get(): MediaOptimizationConfig suspend fun get(): MediaOptimizationConfig
} }
/**
* Creates a [MediaOptimizationConfigProvider] for a given session, used outside the session scope, for instance when sharing to another account.
*/
fun interface MediaOptimizationConfigProviderFactory {
/**
* Create a [MediaOptimizationConfigProvider] using the preferences of the session of [client].
*
* @param client the client of the session whose preferences should be used.
*/
fun create(client: MatrixClient): MediaOptimizationConfigProvider
}
@@ -9,6 +9,7 @@
package io.element.android.libraries.mediaupload.api package io.element.android.libraries.mediaupload.api
import android.net.Uri import android.net.Uri
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
@@ -34,9 +35,11 @@ fun interface MediaSenderRoomFactory {
/** /**
* Create a [MediaSender] for the given [JoinedRoom], with timeline mode Live. * Create a [MediaSender] for the given [JoinedRoom], with timeline mode Live.
* *
* @param client the client of the session the room belongs to, used to read the media preferences of that session.
* @param room the room the media should be sent to. * @param room the room the media should be sent to.
*/ */
fun create( fun create(
client: MatrixClient,
room: JoinedRoom, room: JoinedRoom,
): MediaSender ): MediaSender
} }
@@ -0,0 +1,29 @@
/*
* 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.libraries.mediaupload.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProviderFactory
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
@ContributesBinding(AppScope::class)
class DefaultMediaOptimizationConfigProviderFactory(
private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory,
private val featureFlagService: FeatureFlagService,
) : MediaOptimizationConfigProviderFactory {
override fun create(client: MatrixClient): MediaOptimizationConfigProvider {
return DefaultMediaOptimizationConfigProvider(
sessionPreferencesStore = sessionPreferencesStoreFactory.get(client.sessionId, client.sessionCoroutineScope),
featureFlagsService = featureFlagService,
)
}
}
@@ -9,12 +9,13 @@
package io.element.android.libraries.mediaupload.impl package io.element.android.libraries.mediaupload.impl
import android.net.Uri import android.net.Uri
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.hash.hash import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.extensions.flatMapCatching import io.element.android.libraries.core.extensions.flatMapCatching
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.CreateTimelineParams
@@ -22,6 +23,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProviderFactory
import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.api.MediaSenderFactory import io.element.android.libraries.mediaupload.api.MediaSenderFactory
@@ -52,19 +54,20 @@ class DefaultMediaSenderFactory(
} }
} }
@ContributesBinding(SessionScope::class) @ContributesBinding(AppScope::class)
class DefaultMediaSenderRoomFactory( class DefaultMediaSenderRoomFactory(
private val preProcessor: MediaPreProcessor, private val preProcessor: MediaPreProcessor,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, private val mediaOptimizationConfigProviderFactory: MediaOptimizationConfigProviderFactory,
) : MediaSenderRoomFactory { ) : MediaSenderRoomFactory {
override fun create( override fun create(
client: MatrixClient,
room: JoinedRoom, room: JoinedRoom,
): MediaSender { ): MediaSender {
return DefaultMediaSender( return DefaultMediaSender(
preProcessor = preProcessor, preProcessor = preProcessor,
room = room, room = room,
timelineMode = Timeline.Mode.Live, timelineMode = Timeline.Mode.Live,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, mediaOptimizationConfigProvider = mediaOptimizationConfigProviderFactory.create(client),
) )
} }
} }