Merge pull request #5722 from element-hq/feature/bma/moduleCleanup
Module cleanup
This commit is contained in:
@@ -64,6 +64,7 @@ dependencies {
|
|||||||
testImplementation(projects.features.forward.test)
|
testImplementation(projects.features.forward.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
|
testImplementation(projects.services.appnavstate.impl)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
|||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
|
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -108,7 +109,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||||
spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(),
|
spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(),
|
||||||
forwardEntryPoint: ForwardEntryPoint = FakeForwardEntryPoint(),
|
forwardEntryPoint: ForwardEntryPoint = FakeForwardEntryPoint(),
|
||||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
|
||||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||||
) = JoinedRoomLoadedFlowNode(
|
) = JoinedRoomLoadedFlowNode(
|
||||||
buildContext = BuildContext.root(savedStateMap = null),
|
buildContext = BuildContext.root(savedStateMap = null),
|
||||||
@@ -192,7 +193,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
||||||
val activeRoomsHolder = ActiveRoomsHolder()
|
val activeRoomsHolder = DefaultActiveRoomsHolder()
|
||||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
|
plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
|
||||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
@@ -215,7 +216,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
||||||
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
val activeRoomsHolder = DefaultActiveRoomsHolder().apply {
|
||||||
addRoom(room)
|
addRoom(room)
|
||||||
}
|
}
|
||||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import extension.setupDependencyInjection
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
@@ -16,8 +14,6 @@ android {
|
|||||||
namespace = "io.element.android.features.cachecleaner.api"
|
namespace = "io.element.android.features.cachecleaner.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(libs.androidx.startup)
|
implementation(libs.androidx.startup)
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ dependencies {
|
|||||||
testImplementation(projects.libraries.matrixmedia.test)
|
testImplementation(projects.libraries.matrixmedia.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
testImplementation(projects.services.appnavstate.impl)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
|||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
|
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ class DefaultCallWidgetProviderTest {
|
|||||||
// No room from the client
|
// No room from the client
|
||||||
givenGetRoomResult(A_ROOM_ID, null)
|
givenGetRoomResult(A_ROOM_ID, null)
|
||||||
}
|
}
|
||||||
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
val activeRoomsHolder = DefaultActiveRoomsHolder().apply {
|
||||||
// A current active room with the same room id
|
// A current active room with the same room id
|
||||||
addRoom(
|
addRoom(
|
||||||
FakeJoinedRoom(
|
FakeJoinedRoom(
|
||||||
@@ -130,7 +131,7 @@ class DefaultCallWidgetProviderTest {
|
|||||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
||||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
|
||||||
) = DefaultCallWidgetProvider(
|
) = DefaultCallWidgetProvider(
|
||||||
matrixClientsProvider = matrixClientProvider,
|
matrixClientsProvider = matrixClientProvider,
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ dependencies {
|
|||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
|
testImplementation(projects.libraries.mediaupload.impl)
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
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.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.libraries.mediaupload.api.allFiles
|
import io.element.android.libraries.mediaupload.api.allFiles
|
||||||
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
||||||
@@ -56,7 +56,7 @@ class AttachmentsPreviewPresenter(
|
|||||||
@Assisted private val onDoneListener: OnDoneListener,
|
@Assisted private val onDoneListener: OnDoneListener,
|
||||||
@Assisted private val timelineMode: Timeline.Mode,
|
@Assisted private val timelineMode: Timeline.Mode,
|
||||||
@Assisted private val inReplyToEventId: EventId?,
|
@Assisted private val inReplyToEventId: EventId?,
|
||||||
mediaSenderFactory: MediaSender.Factory,
|
mediaSenderFactory: MediaSenderFactory,
|
||||||
private val permalinkBuilder: PermalinkBuilder,
|
private val permalinkBuilder: PermalinkBuilder,
|
||||||
private val temporaryUriDeleter: TemporaryUriDeleter,
|
private val temporaryUriDeleter: TemporaryUriDeleter,
|
||||||
private val mediaOptimizationSelectorPresenterFactory: MediaOptimizationSelectorPresenter.Factory,
|
private val mediaOptimizationSelectorPresenterFactory: MediaOptimizationSelectorPresenter.Factory,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
|||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
|
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
|
||||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
@@ -107,7 +107,7 @@ class MessageComposerPresenter(
|
|||||||
private val mediaPickerProvider: PickerProvider,
|
private val mediaPickerProvider: PickerProvider,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
private val localMediaFactory: LocalMediaFactory,
|
private val localMediaFactory: LocalMediaFactory,
|
||||||
private val mediaSenderFactory: MediaSender.Factory,
|
mediaSenderFactory: MediaSenderFactory,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val locationService: LocationService,
|
private val locationService: LocationService,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
|
|||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
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.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||||
@@ -57,7 +57,7 @@ class DefaultVoiceMessageComposerPresenter(
|
|||||||
@Assisted private val timelineMode: Timeline.Mode,
|
@Assisted private val timelineMode: Timeline.Mode,
|
||||||
private val voiceRecorder: VoiceRecorder,
|
private val voiceRecorder: VoiceRecorder,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
mediaSenderFactory: MediaSender.Factory,
|
mediaSenderFactory: MediaSenderFactory,
|
||||||
private val player: VoiceMessageComposerPlayer,
|
private val player: VoiceMessageComposerPlayer,
|
||||||
private val messageComposerContext: MessageComposerContext,
|
private val messageComposerContext: MessageComposerContext,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory
|
permissionsPresenterFactory: PermissionsPresenter.Factory
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
|||||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
@@ -166,6 +165,7 @@ class MessagesPresenterTest {
|
|||||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: EventOrTransactionId -> Result.success(true) }
|
val toggleReactionSuccess = lambdaRecorder { _: String, _: EventOrTransactionId -> Result.success(true) }
|
||||||
val toggleReactionFailure =
|
val toggleReactionFailure =
|
||||||
lambdaRecorder { _: String, _: EventOrTransactionId -> Result.failure<Boolean>(IllegalStateException("Failed to send reaction")) }
|
lambdaRecorder { _: String, _: EventOrTransactionId -> Result.failure<Boolean>(IllegalStateException("Failed to send reaction")) }
|
||||||
|
val addRecentEmojiResult = lambdaRecorder { _: String -> Result.success(Unit) }
|
||||||
|
|
||||||
val timeline = FakeTimeline().apply {
|
val timeline = FakeTimeline().apply {
|
||||||
this.toggleReactionLambda = toggleReactionSuccess
|
this.toggleReactionLambda = toggleReactionSuccess
|
||||||
@@ -184,7 +184,8 @@ class MessagesPresenterTest {
|
|||||||
val presenter = createMessagesPresenter(
|
val presenter = createMessagesPresenter(
|
||||||
timeline = timeline,
|
timeline = timeline,
|
||||||
joinedRoom = room,
|
joinedRoom = room,
|
||||||
coroutineDispatchers = coroutineDispatchers
|
addRecentEmoji = AddRecentEmoji { addRecentEmojiResult(it) },
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
)
|
)
|
||||||
presenter.testWithLifecycleOwner {
|
presenter.testWithLifecycleOwner {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
@@ -201,6 +202,7 @@ class MessagesPresenterTest {
|
|||||||
assert(toggleReactionFailure)
|
assert(toggleReactionFailure)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId()))
|
.with(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId()))
|
||||||
|
addRecentEmojiResult.assertions().isCalledOnce().with(value("👍"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +214,9 @@ class MessagesPresenterTest {
|
|||||||
toggle = !toggle
|
toggle = !toggle
|
||||||
Result.success(toggle)
|
Result.success(toggle)
|
||||||
}
|
}
|
||||||
|
val addRecentEmoji = lambdaRecorder { _: String ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
val timeline = FakeTimeline().apply {
|
val timeline = FakeTimeline().apply {
|
||||||
this.toggleReactionLambda = toggleReactionSuccess
|
this.toggleReactionLambda = toggleReactionSuccess
|
||||||
}
|
}
|
||||||
@@ -230,6 +234,7 @@ class MessagesPresenterTest {
|
|||||||
val presenter = createMessagesPresenter(
|
val presenter = createMessagesPresenter(
|
||||||
timeline = timeline,
|
timeline = timeline,
|
||||||
joinedRoom = room,
|
joinedRoom = room,
|
||||||
|
addRecentEmoji = AddRecentEmoji { addRecentEmoji(it) },
|
||||||
coroutineDispatchers = coroutineDispatchers
|
coroutineDispatchers = coroutineDispatchers
|
||||||
)
|
)
|
||||||
presenter.testWithLifecycleOwner {
|
presenter.testWithLifecycleOwner {
|
||||||
@@ -244,6 +249,7 @@ class MessagesPresenterTest {
|
|||||||
listOf(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId())),
|
listOf(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId())),
|
||||||
)
|
)
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
addRecentEmoji.assertions().isCalledOnce().with(value("👍"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1196,10 +1202,12 @@ class MessagesPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.testWithLifecycleOwner {
|
presenter.testWithLifecycleOwner {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(
|
initialState.eventSink(
|
||||||
action = TimelineItemAction.ReplyInThread,
|
MessagesEvents.HandleAction(
|
||||||
event = aMessageEvent(threadInfo = TimelineItemThreadInfo.ThreadResponse(A_THREAD_ID))
|
action = TimelineItemAction.ReplyInThread,
|
||||||
))
|
event = aMessageEvent(threadInfo = TimelineItemThreadInfo.ThreadResponse(A_THREAD_ID))
|
||||||
|
)
|
||||||
|
)
|
||||||
awaitItem()
|
awaitItem()
|
||||||
openThreadLambda.assertions().isCalledOnce().with(value(A_THREAD_ID), value(null))
|
openThreadLambda.assertions().isCalledOnce().with(value(A_THREAD_ID), value(null))
|
||||||
}
|
}
|
||||||
@@ -1216,14 +1224,16 @@ class MessagesPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.testWithLifecycleOwner {
|
presenter.testWithLifecycleOwner {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(
|
initialState.eventSink(
|
||||||
action = TimelineItemAction.ReplyInThread,
|
MessagesEvents.HandleAction(
|
||||||
event = aMessageEvent(
|
action = TimelineItemAction.ReplyInThread,
|
||||||
// The event id will be used as the thread id instead
|
event = aMessageEvent(
|
||||||
eventId = AN_EVENT_ID,
|
// The event id will be used as the thread id instead
|
||||||
threadInfo = null,
|
eventId = AN_EVENT_ID,
|
||||||
|
threadInfo = null,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
awaitItem()
|
awaitItem()
|
||||||
openThreadLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID.toThreadId()), value(null))
|
openThreadLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID.toThreadId()), value(null))
|
||||||
}
|
}
|
||||||
@@ -1334,7 +1344,7 @@ class MessagesPresenterTest {
|
|||||||
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
||||||
actionListEventSink: (ActionListEvents) -> Unit = {},
|
actionListEventSink: (ActionListEvents) -> Unit = {},
|
||||||
addRecentEmoji: AddRecentEmoji = AddRecentEmoji(FakeMatrixClient(), testCoroutineDispatchers()),
|
addRecentEmoji: AddRecentEmoji = AddRecentEmoji { _ -> lambdaError() },
|
||||||
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(),
|
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(),
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
|||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||||
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.MediaSenderFactory
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
|
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
|
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
|
||||||
@@ -601,17 +602,15 @@ class AttachmentsPreviewPresenterTest {
|
|||||||
return AttachmentsPreviewPresenter(
|
return AttachmentsPreviewPresenter(
|
||||||
attachment = aMediaAttachment(localMedia),
|
attachment = aMediaAttachment(localMedia),
|
||||||
onDoneListener = onDoneListener,
|
onDoneListener = onDoneListener,
|
||||||
mediaSenderFactory = object : MediaSender.Factory {
|
mediaSenderFactory = MediaSenderFactory { timelineMode ->
|
||||||
override fun create(timelineMode: Timeline.Mode): MediaSender {
|
DefaultMediaSender(
|
||||||
return MediaSender(
|
preProcessor = mediaPreProcessor,
|
||||||
preProcessor = mediaPreProcessor,
|
room = room,
|
||||||
room = room,
|
timelineMode = timelineMode,
|
||||||
timelineMode = timelineMode,
|
mediaOptimizationConfigProvider = {
|
||||||
mediaOptimizationConfigProvider = {
|
MediaOptimizationConfig(compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD)
|
||||||
MediaOptimizationConfig(compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD)
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
temporaryUriDeleter = temporaryUriDeleter,
|
temporaryUriDeleter = temporaryUriDeleter,
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import io.element.android.libraries.architecture.AsyncData
|
|||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.mediaupload.api.MaxUploadSizeProvider
|
import io.element.android.libraries.mediaupload.api.MaxUploadSizeProvider
|
||||||
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||||
@@ -206,7 +205,7 @@ class DefaultMediaOptimizationSelectorPresenterTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `present - max upload size will default to 100MB if we can't get it`() = runTest {
|
fun `present - max upload size will default to 100MB if we can't get it`() = runTest {
|
||||||
val presenter = createDefaultMediaOptimizationSelectorPresenter(
|
val presenter = createDefaultMediaOptimizationSelectorPresenter(
|
||||||
maxUploadSizeProvider = MaxUploadSizeProvider(FakeMatrixClient(getMaxUploadSizeResult = { Result.failure(AN_EXCEPTION) }))
|
maxUploadSizeProvider = MaxUploadSizeProvider { Result.failure(AN_EXCEPTION) }
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
@@ -233,9 +232,7 @@ class DefaultMediaOptimizationSelectorPresenterTest {
|
|||||||
|
|
||||||
private fun createDefaultMediaOptimizationSelectorPresenter(
|
private fun createDefaultMediaOptimizationSelectorPresenter(
|
||||||
localMedia: LocalMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()),
|
localMedia: LocalMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()),
|
||||||
maxUploadSizeProvider: MaxUploadSizeProvider = MaxUploadSizeProvider(
|
maxUploadSizeProvider: MaxUploadSizeProvider = MaxUploadSizeProvider { Result.success(1_000L) },
|
||||||
FakeMatrixClient(getMaxUploadSizeResult = { Result.success(1_000L) }),
|
|
||||||
),
|
|
||||||
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SelectableMediaQuality.key to true)),
|
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SelectableMediaQuality.key to true)),
|
||||||
mediaExtractorFactory: FakeVideoMetadataExtractorFactory = FakeVideoMetadataExtractorFactory(),
|
mediaExtractorFactory: FakeVideoMetadataExtractorFactory = FakeVideoMetadataExtractorFactory(),
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ import io.element.android.libraries.mediapickers.api.PickerProvider
|
|||||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||||
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.MediaSenderFactory
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
|
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
|
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
||||||
@@ -1551,20 +1552,18 @@ class MessageComposerPresenterTest {
|
|||||||
mediaPickerProvider = pickerProvider,
|
mediaPickerProvider = pickerProvider,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
localMediaFactory = localMediaFactory,
|
localMediaFactory = localMediaFactory,
|
||||||
mediaSenderFactory = object : MediaSender.Factory {
|
mediaSenderFactory = MediaSenderFactory { timelineMode ->
|
||||||
override fun create(timelineMode: Timeline.Mode): MediaSender {
|
DefaultMediaSender(
|
||||||
return MediaSender(
|
preProcessor = mediaPreProcessor,
|
||||||
preProcessor = mediaPreProcessor,
|
room = room,
|
||||||
room = room,
|
timelineMode = timelineMode,
|
||||||
timelineMode = timelineMode,
|
mediaOptimizationConfigProvider = {
|
||||||
mediaOptimizationConfigProvider = {
|
MediaOptimizationConfig(
|
||||||
MediaOptimizationConfig(
|
|
||||||
compressImages = true,
|
compressImages = true,
|
||||||
videoCompressionPreset = VideoCompressionPreset.STANDARD
|
videoCompressionPreset = VideoCompressionPreset.STANDARD
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
snackbarDispatcher = snackbarDispatcher,
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
|||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
||||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||||
@@ -75,7 +75,7 @@ class VoiceMessageComposerPresenterTest {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
|
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
|
||||||
private val mediaSender = MediaSender(
|
private val mediaSender = DefaultMediaSender(
|
||||||
preProcessor = mediaPreProcessor,
|
preProcessor = mediaPreProcessor,
|
||||||
room = joinedRoom,
|
room = joinedRoom,
|
||||||
timelineMode = Timeline.Mode.Live,
|
timelineMode = Timeline.Mode.Live,
|
||||||
@@ -668,11 +668,7 @@ class VoiceMessageComposerPresenterTest {
|
|||||||
timelineMode = Timeline.Mode.Live,
|
timelineMode = Timeline.Mode.Live,
|
||||||
voiceRecorder = voiceRecorder,
|
voiceRecorder = voiceRecorder,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
mediaSenderFactory = object : MediaSender.Factory {
|
mediaSenderFactory = { mediaSender },
|
||||||
override fun create(timelineMode: Timeline.Mode): MediaSender {
|
|
||||||
return mediaSender
|
|
||||||
}
|
|
||||||
},
|
|
||||||
player = VoiceMessageComposerPlayer(FakeMediaPlayer(), this),
|
player = VoiceMessageComposerPlayer(FakeMediaPlayer(), this),
|
||||||
messageComposerContext = messageComposerContext,
|
messageComposerContext = messageComposerContext,
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ dependencies {
|
|||||||
implementation(projects.libraries.voicerecorder.test)
|
implementation(projects.libraries.voicerecorder.test)
|
||||||
implementation(projects.services.analytics.test)
|
implementation(projects.services.analytics.test)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
|
implementation(projects.libraries.mediaupload.impl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
|||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
|
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
@@ -24,7 +25,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
|
|
||||||
class FakeDefaultVoiceMessageComposerPresenterFactory(
|
class FakeDefaultVoiceMessageComposerPresenterFactory(
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val mediaSender: MediaSender = MediaSender(
|
private val mediaSender: MediaSender = DefaultMediaSender(
|
||||||
preProcessor = FakeMediaPreProcessor(),
|
preProcessor = FakeMediaPreProcessor(),
|
||||||
room = FakeJoinedRoom(),
|
room = FakeJoinedRoom(),
|
||||||
timelineMode = Timeline.Mode.Live,
|
timelineMode = Timeline.Mode.Live,
|
||||||
@@ -37,11 +38,7 @@ class FakeDefaultVoiceMessageComposerPresenterFactory(
|
|||||||
timelineMode = timelineMode,
|
timelineMode = timelineMode,
|
||||||
voiceRecorder = FakeVoiceRecorder(),
|
voiceRecorder = FakeVoiceRecorder(),
|
||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
mediaSenderFactory = object : MediaSender.Factory {
|
mediaSenderFactory = { mediaSender },
|
||||||
override fun create(timelineMode: Timeline.Mode): MediaSender {
|
|
||||||
return mediaSender
|
|
||||||
}
|
|
||||||
},
|
|
||||||
player = VoiceMessageComposerPlayer(
|
player = VoiceMessageComposerPlayer(
|
||||||
mediaPlayer = FakeMediaPlayer(),
|
mediaPlayer = FakeMediaPlayer(),
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ dependencies {
|
|||||||
testImplementation(projects.libraries.indicator.test)
|
testImplementation(projects.libraries.indicator.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
testImplementation(projects.libraries.sessionStorage.test)
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
|
testImplementation(projects.services.appnavstate.impl)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|||||||
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.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
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.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
@@ -34,7 +34,7 @@ import org.robolectric.RobolectricTestRunner
|
|||||||
class DefaultClearCacheUseCaseTest {
|
class DefaultClearCacheUseCaseTest {
|
||||||
@Test
|
@Test
|
||||||
fun `execute clear cache should do all the expected tasks`() = runTest {
|
fun `execute clear cache should do all the expected tasks`() = runTest {
|
||||||
val activeRoomsHolder = ActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) }
|
val activeRoomsHolder = DefaultActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) }
|
||||||
val clearCacheLambda = lambdaRecorder<Unit> { }
|
val clearCacheLambda = lambdaRecorder<Unit> { }
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
|
|||||||
@@ -48,4 +48,5 @@ dependencies {
|
|||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.roomselect.test)
|
testImplementation(projects.libraries.roomselect.test)
|
||||||
|
testImplementation(projects.services.appnavstate.impl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
|||||||
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.RoomId
|
||||||
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.mediaupload.api.MediaOptimizationConfigProvider
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
|
||||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -39,7 +37,7 @@ class SharePresenter(
|
|||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val shareIntentHandler: ShareIntentHandler,
|
private val shareIntentHandler: ShareIntentHandler,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val mediaPreProcessor: MediaPreProcessor,
|
private val mediaSenderRoomFactory: MediaSenderRoomFactory,
|
||||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||||
) : Presenter<ShareState> {
|
) : Presenter<ShareState> {
|
||||||
@@ -88,12 +86,7 @@ class SharePresenter(
|
|||||||
roomIds
|
roomIds
|
||||||
.map { roomId ->
|
.map { roomId ->
|
||||||
val room = getJoinedRoom(roomId) ?: return@map false
|
val room = getJoinedRoom(roomId) ?: return@map false
|
||||||
val mediaSender = MediaSender(
|
val mediaSender = mediaSenderRoomFactory.create(room = room)
|
||||||
preProcessor = mediaPreProcessor,
|
|
||||||
room = room,
|
|
||||||
timelineMode = Timeline.Mode.Live,
|
|
||||||
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
|
||||||
)
|
|
||||||
filesToShare
|
filesToShare
|
||||||
.map { fileToShare ->
|
.map { fileToShare ->
|
||||||
val result = mediaSender.sendMedia(
|
val result = mediaSender.sendMedia(
|
||||||
|
|||||||
@@ -17,18 +17,17 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
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.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.EventId
|
|
||||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
|
||||||
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.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
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.FakeMediaOptimizationConfigProvider
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaSender
|
||||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
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 kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
@@ -37,7 +36,6 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class SharePresenterTest {
|
class SharePresenterTest {
|
||||||
@@ -121,18 +119,16 @@ class SharePresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - send media ok`() = runTest {
|
fun `present - send media ok`() = runTest {
|
||||||
val sendFileResult =
|
val sendMediaResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
lambdaRecorder<File, FileInfo, String?, String?, EventId?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
|
||||||
Result.success(FakeMediaUploadHandler())
|
|
||||||
}
|
|
||||||
val joinedRoom = FakeJoinedRoom(
|
val joinedRoom = FakeJoinedRoom(
|
||||||
liveTimeline = FakeTimeline().apply {
|
liveTimeline = FakeTimeline(),
|
||||||
sendFileLambda = sendFileResult
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, joinedRoom)
|
givenGetRoomResult(A_ROOM_ID, joinedRoom)
|
||||||
}
|
}
|
||||||
|
val mediaSender = FakeMediaSender(
|
||||||
|
sendMediaResult = sendMediaResult,
|
||||||
|
)
|
||||||
val presenter = createSharePresenter(
|
val presenter = createSharePresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
shareIntentHandler = FakeShareIntentHandler { _, onFile, _ ->
|
shareIntentHandler = FakeShareIntentHandler { _, onFile, _ ->
|
||||||
@@ -144,7 +140,8 @@ class SharePresenterTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
mediaSenderRoomFactory = MediaSenderRoomFactory { mediaSender },
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
@@ -156,7 +153,7 @@ class SharePresenterTest {
|
|||||||
val success = awaitItem()
|
val success = awaitItem()
|
||||||
assertThat(success.shareAction.isSuccess()).isTrue()
|
assertThat(success.shareAction.isSuccess()).isTrue()
|
||||||
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
|
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
|
||||||
sendFileResult.assertions().isCalledOnce()
|
sendMediaResult.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,17 +162,17 @@ internal fun TestScope.createSharePresenter(
|
|||||||
intent: Intent = Intent(),
|
intent: Intent = Intent(),
|
||||||
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
|
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
|
||||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
|
||||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
mediaSenderRoomFactory: MediaSenderRoomFactory = MediaSenderRoomFactory { FakeMediaSender() },
|
||||||
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
||||||
): SharePresenter {
|
): SharePresenter {
|
||||||
return SharePresenter(
|
return SharePresenter(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
sessionCoroutineScope = this,
|
sessionCoroutineScope = this,
|
||||||
shareIntentHandler = shareIntentHandler,
|
shareIntentHandler = shareIntentHandler,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
mediaPreProcessor = mediaPreProcessor,
|
|
||||||
activeRoomsHolder = activeRoomsHolder,
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
|
mediaSenderRoomFactory = mediaSenderRoomFactory,
|
||||||
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import config.BuildTimeConfig
|
import config.BuildTimeConfig
|
||||||
import extension.buildConfigFieldStr
|
import extension.buildConfigFieldStr
|
||||||
import extension.setupDependencyInjection
|
|
||||||
import extension.testCommonDependencies
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -17,8 +16,6 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.matrix.api"
|
namespace = "io.element.android.libraries.matrix.api"
|
||||||
|
|
||||||
|
|||||||
@@ -8,28 +8,12 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.matrix.api.mxc
|
package io.element.android.libraries.matrix.api.mxc
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
interface MxcTools {
|
||||||
|
|
||||||
@Inject
|
|
||||||
class MxcTools {
|
|
||||||
/**
|
|
||||||
* Regex to match a Matrix Content (mxc://) URI.
|
|
||||||
*
|
|
||||||
* See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris
|
|
||||||
*/
|
|
||||||
private val mxcRegex = Regex("""^mxc://([^/]+)/([^/]+)$""")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitizes an mxcUri to be used as a relative file path.
|
* Sanitizes an mxcUri to be used as a relative file path.
|
||||||
*
|
*
|
||||||
* @param mxcUri the Matrix Content (mxc://) URI of the file.
|
* @param mxcUri the Matrix Content (mxc://) URI of the file.
|
||||||
* @return the relative file path as "<server-name>/<media-id>" or null if the mxcUri is invalid.
|
* @return the relative file path as "<server-name>/<media-id>" or null if the mxcUri is invalid.
|
||||||
*/
|
*/
|
||||||
fun mxcUri2FilePath(mxcUri: String): String? = mxcRegex.matchEntire(mxcUri)?.let { match ->
|
fun mxcUri2FilePath(mxcUri: String): String?
|
||||||
buildString {
|
|
||||||
append(match.groupValues[1])
|
|
||||||
append("/")
|
|
||||||
append(match.groupValues[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 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.matrix.impl.mxc
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultMxcTools : MxcTools {
|
||||||
|
/**
|
||||||
|
* Regex to match a Matrix Content (mxc://) URI.
|
||||||
|
*
|
||||||
|
* See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris
|
||||||
|
*/
|
||||||
|
private val mxcRegex = Regex("""^mxc://([^/]+)/([^/]+)$""")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes an mxcUri to be used as a relative file path.
|
||||||
|
*
|
||||||
|
* @param mxcUri the Matrix Content (mxc://) URI of the file.
|
||||||
|
* @return the relative file path as "<server-name>/<media-id>" or null if the mxcUri is invalid.
|
||||||
|
*/
|
||||||
|
override fun mxcUri2FilePath(mxcUri: String): String? = mxcRegex.matchEntire(mxcUri)?.let { match ->
|
||||||
|
buildString {
|
||||||
|
append(match.groupValues[1])
|
||||||
|
append("/")
|
||||||
|
append(match.groupValues[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,15 +6,15 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.mxc
|
package io.element.android.libraries.matrix.impl.mxc
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class MxcToolsTest {
|
class DefaultMxcToolsTest {
|
||||||
@Test
|
@Test
|
||||||
fun `mxcUri2FilePath returns extracted path`() {
|
fun `mxcUri2FilePath returns extracted path`() {
|
||||||
val mxcTools = MxcTools()
|
val mxcTools = DefaultMxcTools()
|
||||||
val mxcUri = "mxc://server.org/abc123"
|
val mxcUri = "mxc://server.org/abc123"
|
||||||
val filePath = mxcTools.mxcUri2FilePath(mxcUri)
|
val filePath = mxcTools.mxcUri2FilePath(mxcUri)
|
||||||
assertThat(filePath).isEqualTo("server.org/abc123")
|
assertThat(filePath).isEqualTo("server.org/abc123")
|
||||||
@@ -22,7 +22,7 @@ class MxcToolsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `mxcUri2FilePath returns null for invalid data`() {
|
fun `mxcUri2FilePath returns null for invalid data`() {
|
||||||
val mxcTools = MxcTools()
|
val mxcTools = DefaultMxcTools()
|
||||||
assertThat(mxcTools.mxcUri2FilePath("")).isNull()
|
assertThat(mxcTools.mxcUri2FilePath("")).isNull()
|
||||||
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org")).isNull()
|
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org")).isNull()
|
||||||
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org/")).isNull()
|
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org/")).isNull()
|
||||||
@@ -19,6 +19,7 @@ dependencies {
|
|||||||
api(projects.libraries.matrix.api)
|
api(projects.libraries.matrix.api)
|
||||||
api(libs.coroutines.core)
|
api(libs.coroutines.core)
|
||||||
implementation(libs.coroutines.test)
|
implementation(libs.coroutines.test)
|
||||||
|
implementation(projects.libraries.matrix.impl)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
implementation(libs.kotlinx.collections.immutable)
|
implementation(libs.kotlinx.collections.immutable)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 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.matrix.test.mxc
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
||||||
|
import io.element.android.libraries.matrix.impl.mxc.DefaultMxcTools
|
||||||
|
|
||||||
|
class FakeMxcTools(
|
||||||
|
private val delegate: MxcTools = DefaultMxcTools()
|
||||||
|
) : MxcTools by delegate
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import extension.setupDependencyInjection
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
@@ -12,8 +10,6 @@ plugins {
|
|||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.mediapickers.test"
|
namespace = "io.element.android.libraries.mediapickers.test"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import extension.setupDependencyInjection
|
|
||||||
import extension.testCommonDependencies
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -13,8 +12,6 @@ plugins {
|
|||||||
id("io.element.android-library")
|
id("io.element.android-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.mediaupload.api"
|
namespace = "io.element.android.libraries.mediaupload.api"
|
||||||
}
|
}
|
||||||
@@ -27,9 +24,4 @@ dependencies {
|
|||||||
api(projects.libraries.matrix.api)
|
api(projects.libraries.matrix.api)
|
||||||
api(projects.libraries.preferences.api)
|
api(projects.libraries.preferences.api)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
|
|
||||||
testCommonDependencies(libs)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
|
||||||
testImplementation(projects.libraries.preferences.test)
|
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,9 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.mediaupload.api
|
package io.element.android.libraries.mediaupload.api
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the maximum upload size allowed by the Matrix server.
|
* Provides the maximum upload size allowed by the Matrix server.
|
||||||
*/
|
*/
|
||||||
@Inject
|
fun interface MaxUploadSizeProvider {
|
||||||
class MaxUploadSizeProvider(
|
suspend fun getMaxUploadSize(): Result<Long>
|
||||||
private val matrixClient: MatrixClient,
|
|
||||||
) {
|
|
||||||
suspend fun getMaxUploadSize(): Result<Long> {
|
|
||||||
return matrixClient.getMaxFileUploadSize()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,73 +9,41 @@
|
|||||||
package io.element.android.libraries.mediaupload.api
|
package io.element.android.libraries.mediaupload.api
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import dev.zacsweers.metro.Assisted
|
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
|
||||||
import dev.zacsweers.metro.AssistedInject
|
|
||||||
import io.element.android.libraries.androidutils.hash.hash
|
|
||||||
import io.element.android.libraries.core.extensions.flatMap
|
|
||||||
import io.element.android.libraries.core.extensions.flatMapCatching
|
|
||||||
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.room.CreateTimelineParams
|
|
||||||
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
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
@AssistedInject
|
fun interface MediaSenderFactory {
|
||||||
class MediaSender(
|
/**
|
||||||
private val preProcessor: MediaPreProcessor,
|
* Create a [MediaSender] for the given [Timeline.Mode], in the Room Scope.
|
||||||
private val room: JoinedRoom,
|
*/
|
||||||
@Assisted private val timelineMode: Timeline.Mode,
|
fun create(
|
||||||
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
timelineMode: Timeline.Mode,
|
||||||
) {
|
): MediaSender
|
||||||
@AssistedFactory
|
}
|
||||||
interface Factory {
|
|
||||||
fun create(
|
|
||||||
timelineMode: Timeline.Mode,
|
|
||||||
): MediaSender
|
|
||||||
}
|
|
||||||
|
|
||||||
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
fun interface MediaSenderRoomFactory {
|
||||||
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
/**
|
||||||
|
* Create a [MediaSender] for the given [JoinedRoom], with timeline mode Live.
|
||||||
|
*/
|
||||||
|
fun create(
|
||||||
|
room: JoinedRoom,
|
||||||
|
): MediaSender
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MediaSender {
|
||||||
suspend fun preProcessMedia(
|
suspend fun preProcessMedia(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||||
): Result<MediaUploadInfo> {
|
): Result<MediaUploadInfo>
|
||||||
Timber.d("Pre-processing media | uri: ${mediaId(uri)} | mimeType: $mimeType")
|
|
||||||
return preProcessor
|
|
||||||
.process(
|
|
||||||
uri = uri,
|
|
||||||
mimeType = mimeType,
|
|
||||||
deleteOriginal = false,
|
|
||||||
mediaOptimizationConfig = mediaOptimizationConfig,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendPreProcessedMedia(
|
suspend fun sendPreProcessedMedia(
|
||||||
mediaUploadInfo: MediaUploadInfo,
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
caption: String?,
|
caption: String?,
|
||||||
formattedCaption: String?,
|
formattedCaption: String?,
|
||||||
inReplyToEventId: EventId?,
|
inReplyToEventId: EventId?,
|
||||||
): Result<Unit> {
|
): Result<Unit>
|
||||||
val mediaLogId = mediaId(mediaUploadInfo.file)
|
|
||||||
return getTimeline().flatMap {
|
|
||||||
Timber.d("Started sending media $mediaLogId using timeline: ${it.mode}")
|
|
||||||
it.sendMedia(
|
|
||||||
uploadInfo = mediaUploadInfo,
|
|
||||||
caption = caption,
|
|
||||||
formattedCaption = formattedCaption,
|
|
||||||
inReplyToEventId = inReplyToEventId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.handleSendResult(mediaLogId)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendMedia(
|
suspend fun sendMedia(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
@@ -84,147 +52,14 @@ class MediaSender(
|
|||||||
formattedCaption: String? = null,
|
formattedCaption: String? = null,
|
||||||
inReplyToEventId: EventId? = null,
|
inReplyToEventId: EventId? = null,
|
||||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||||
): Result<Unit> {
|
): Result<Unit>
|
||||||
return preProcessor
|
|
||||||
.process(
|
|
||||||
uri = uri,
|
|
||||||
mimeType = mimeType,
|
|
||||||
deleteOriginal = false,
|
|
||||||
mediaOptimizationConfig = mediaOptimizationConfig,
|
|
||||||
)
|
|
||||||
.flatMapCatching { info ->
|
|
||||||
getTimeline().getOrThrow().sendMedia(
|
|
||||||
uploadInfo = info,
|
|
||||||
caption = caption,
|
|
||||||
formattedCaption = formattedCaption,
|
|
||||||
inReplyToEventId = inReplyToEventId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.handleSendResult(mediaId(uri))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendVoiceMessage(
|
suspend fun sendVoiceMessage(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
waveForm: List<Float>,
|
waveForm: List<Float>,
|
||||||
inReplyToEventId: EventId? = null,
|
inReplyToEventId: EventId? = null,
|
||||||
): Result<Unit> {
|
): Result<Unit>
|
||||||
return preProcessor
|
|
||||||
.process(
|
|
||||||
uri = uri,
|
|
||||||
mimeType = mimeType,
|
|
||||||
deleteOriginal = true,
|
|
||||||
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
|
|
||||||
)
|
|
||||||
.flatMapCatching { info ->
|
|
||||||
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
|
|
||||||
val newInfo = MediaUploadInfo.VoiceMessage(
|
|
||||||
file = info.file,
|
|
||||||
audioInfo = audioInfo,
|
|
||||||
waveform = waveForm,
|
|
||||||
)
|
|
||||||
getTimeline().getOrThrow().sendMedia(
|
|
||||||
uploadInfo = newInfo,
|
|
||||||
caption = null,
|
|
||||||
formattedCaption = null,
|
|
||||||
inReplyToEventId = inReplyToEventId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.handleSendResult(mediaId(uri))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Result<Unit>.handleSendResult(mediaId: String) = this
|
fun cleanUp()
|
||||||
.onFailure { error ->
|
|
||||||
val job = ongoingUploadJobs.remove(Job)
|
|
||||||
Timber.e(error, "Sending media $mediaId failed. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
|
|
||||||
if (error !is CancellationException) {
|
|
||||||
job?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onSuccess {
|
|
||||||
Timber.d("Sent media $mediaId successfully. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
|
|
||||||
ongoingUploadJobs.remove(Job)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun Timeline.sendMedia(
|
|
||||||
uploadInfo: MediaUploadInfo,
|
|
||||||
caption: String?,
|
|
||||||
formattedCaption: String?,
|
|
||||||
inReplyToEventId: EventId?,
|
|
||||||
): Result<Unit> {
|
|
||||||
val handler = when (uploadInfo) {
|
|
||||||
is MediaUploadInfo.Image -> {
|
|
||||||
sendImage(
|
|
||||||
file = uploadInfo.file,
|
|
||||||
thumbnailFile = uploadInfo.thumbnailFile,
|
|
||||||
imageInfo = uploadInfo.imageInfo,
|
|
||||||
caption = caption,
|
|
||||||
formattedCaption = formattedCaption,
|
|
||||||
inReplyToEventId = inReplyToEventId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is MediaUploadInfo.Video -> {
|
|
||||||
sendVideo(
|
|
||||||
file = uploadInfo.file,
|
|
||||||
thumbnailFile = uploadInfo.thumbnailFile,
|
|
||||||
videoInfo = uploadInfo.videoInfo,
|
|
||||||
caption = caption,
|
|
||||||
formattedCaption = formattedCaption,
|
|
||||||
inReplyToEventId = inReplyToEventId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is MediaUploadInfo.Audio -> {
|
|
||||||
sendAudio(
|
|
||||||
file = uploadInfo.file,
|
|
||||||
audioInfo = uploadInfo.audioInfo,
|
|
||||||
caption = caption,
|
|
||||||
formattedCaption = formattedCaption,
|
|
||||||
inReplyToEventId = inReplyToEventId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is MediaUploadInfo.VoiceMessage -> {
|
|
||||||
sendVoiceMessage(
|
|
||||||
file = uploadInfo.file,
|
|
||||||
audioInfo = uploadInfo.audioInfo,
|
|
||||||
waveform = uploadInfo.waveform,
|
|
||||||
inReplyToEventId = inReplyToEventId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is MediaUploadInfo.AnyFile -> {
|
|
||||||
sendFile(
|
|
||||||
file = uploadInfo.file,
|
|
||||||
fileInfo = uploadInfo.fileInfo,
|
|
||||||
caption = caption,
|
|
||||||
formattedCaption = formattedCaption,
|
|
||||||
inReplyToEventId = inReplyToEventId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We handle the cancellations here manually, so we suppress the warning
|
|
||||||
@Suppress("RunCatchingNotAllowed")
|
|
||||||
return handler
|
|
||||||
.mapCatching { uploadHandler ->
|
|
||||||
Timber.d("Added ongoing upload job, total: ${ongoingUploadJobs.size + 1}")
|
|
||||||
ongoingUploadJobs[Job] = uploadHandler
|
|
||||||
uploadHandler.await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getTimeline(): Result<Timeline> {
|
|
||||||
return when (timelineMode) {
|
|
||||||
is Timeline.Mode.Thread -> {
|
|
||||||
room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = timelineMode.threadRootId))
|
|
||||||
}
|
|
||||||
else -> Result.success(room.liveTimeline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up any temporary files or resources used during the media processing.
|
|
||||||
*/
|
|
||||||
fun cleanUp() = preProcessor.cleanUp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mediaId(uri: Uri?): String = uri?.path.orEmpty().hash()
|
|
||||||
private fun mediaId(file: File): String = file.path.orEmpty().hash()
|
|
||||||
|
|||||||
@@ -42,4 +42,7 @@ dependencies {
|
|||||||
|
|
||||||
testCommonDependencies(libs)
|
testCommonDependencies(libs)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
* Copyright 2025 New Vector 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.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.mediaupload.api.MaxUploadSizeProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the maximum upload size allowed by the Matrix server.
|
||||||
|
*/
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultMaxUploadSizeProvider(
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
) : MaxUploadSizeProvider {
|
||||||
|
override suspend fun getMaxUploadSize(): Result<Long> {
|
||||||
|
return matrixClient.getMaxFileUploadSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
* Copyright 2023-2025 New Vector 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 android.net.Uri
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.androidutils.hash.hash
|
||||||
|
import io.element.android.libraries.core.extensions.flatMap
|
||||||
|
import io.element.android.libraries.core.extensions.flatMapCatching
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
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.room.CreateTimelineParams
|
||||||
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
@ContributesBinding(RoomScope::class)
|
||||||
|
class DefaultMediaSenderFactory(
|
||||||
|
private val preProcessor: MediaPreProcessor,
|
||||||
|
private val room: JoinedRoom,
|
||||||
|
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||||
|
) : MediaSenderFactory {
|
||||||
|
override fun create(
|
||||||
|
timelineMode: Timeline.Mode,
|
||||||
|
): MediaSender {
|
||||||
|
return DefaultMediaSender(
|
||||||
|
preProcessor = preProcessor,
|
||||||
|
room = room,
|
||||||
|
timelineMode = timelineMode,
|
||||||
|
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultMediaSenderRoomFactory(
|
||||||
|
private val preProcessor: MediaPreProcessor,
|
||||||
|
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||||
|
) : MediaSenderRoomFactory {
|
||||||
|
override fun create(
|
||||||
|
room: JoinedRoom,
|
||||||
|
): MediaSender {
|
||||||
|
return DefaultMediaSender(
|
||||||
|
preProcessor = preProcessor,
|
||||||
|
room = room,
|
||||||
|
timelineMode = Timeline.Mode.Live,
|
||||||
|
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultMediaSender(
|
||||||
|
private val preProcessor: MediaPreProcessor,
|
||||||
|
private val room: JoinedRoom,
|
||||||
|
private val timelineMode: Timeline.Mode,
|
||||||
|
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||||
|
) : MediaSender {
|
||||||
|
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
||||||
|
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
||||||
|
|
||||||
|
override suspend fun preProcessMedia(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||||
|
): Result<MediaUploadInfo> {
|
||||||
|
Timber.d("Pre-processing media | uri: ${mediaId(uri)} | mimeType: $mimeType")
|
||||||
|
return preProcessor
|
||||||
|
.process(
|
||||||
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
|
deleteOriginal = false,
|
||||||
|
mediaOptimizationConfig = mediaOptimizationConfig,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendPreProcessedMedia(
|
||||||
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
inReplyToEventId: EventId?,
|
||||||
|
): Result<Unit> {
|
||||||
|
val mediaLogId = mediaId(mediaUploadInfo.file)
|
||||||
|
return getTimeline().flatMap {
|
||||||
|
Timber.d("Started sending media $mediaLogId using timeline: ${it.mode}")
|
||||||
|
it.sendMedia(
|
||||||
|
uploadInfo = mediaUploadInfo,
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = formattedCaption,
|
||||||
|
inReplyToEventId = inReplyToEventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.handleSendResult(mediaLogId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendMedia(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
inReplyToEventId: EventId?,
|
||||||
|
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||||
|
): Result<Unit> {
|
||||||
|
return preProcessor
|
||||||
|
.process(
|
||||||
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
|
deleteOriginal = false,
|
||||||
|
mediaOptimizationConfig = mediaOptimizationConfig,
|
||||||
|
)
|
||||||
|
.flatMapCatching { info ->
|
||||||
|
getTimeline().getOrThrow().sendMedia(
|
||||||
|
uploadInfo = info,
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = formattedCaption,
|
||||||
|
inReplyToEventId = inReplyToEventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.handleSendResult(mediaId(uri))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendVoiceMessage(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
waveForm: List<Float>,
|
||||||
|
inReplyToEventId: EventId?,
|
||||||
|
): Result<Unit> {
|
||||||
|
return preProcessor
|
||||||
|
.process(
|
||||||
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
|
deleteOriginal = true,
|
||||||
|
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
|
||||||
|
)
|
||||||
|
.flatMapCatching { info ->
|
||||||
|
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
|
||||||
|
val newInfo = MediaUploadInfo.VoiceMessage(
|
||||||
|
file = info.file,
|
||||||
|
audioInfo = audioInfo,
|
||||||
|
waveform = waveForm,
|
||||||
|
)
|
||||||
|
getTimeline().getOrThrow().sendMedia(
|
||||||
|
uploadInfo = newInfo,
|
||||||
|
caption = null,
|
||||||
|
formattedCaption = null,
|
||||||
|
inReplyToEventId = inReplyToEventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.handleSendResult(mediaId(uri))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Result<Unit>.handleSendResult(mediaId: String) = this
|
||||||
|
.onFailure { error ->
|
||||||
|
val job = ongoingUploadJobs.remove(Job)
|
||||||
|
Timber.e(error, "Sending media $mediaId failed. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
|
||||||
|
if (error !is CancellationException) {
|
||||||
|
job?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onSuccess {
|
||||||
|
Timber.d("Sent media $mediaId successfully. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
|
||||||
|
ongoingUploadJobs.remove(Job)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun Timeline.sendMedia(
|
||||||
|
uploadInfo: MediaUploadInfo,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
inReplyToEventId: EventId?,
|
||||||
|
): Result<Unit> {
|
||||||
|
val handler = when (uploadInfo) {
|
||||||
|
is MediaUploadInfo.Image -> {
|
||||||
|
sendImage(
|
||||||
|
file = uploadInfo.file,
|
||||||
|
thumbnailFile = uploadInfo.thumbnailFile,
|
||||||
|
imageInfo = uploadInfo.imageInfo,
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = formattedCaption,
|
||||||
|
inReplyToEventId = inReplyToEventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is MediaUploadInfo.Video -> {
|
||||||
|
sendVideo(
|
||||||
|
file = uploadInfo.file,
|
||||||
|
thumbnailFile = uploadInfo.thumbnailFile,
|
||||||
|
videoInfo = uploadInfo.videoInfo,
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = formattedCaption,
|
||||||
|
inReplyToEventId = inReplyToEventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is MediaUploadInfo.Audio -> {
|
||||||
|
sendAudio(
|
||||||
|
file = uploadInfo.file,
|
||||||
|
audioInfo = uploadInfo.audioInfo,
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = formattedCaption,
|
||||||
|
inReplyToEventId = inReplyToEventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is MediaUploadInfo.VoiceMessage -> {
|
||||||
|
sendVoiceMessage(
|
||||||
|
file = uploadInfo.file,
|
||||||
|
audioInfo = uploadInfo.audioInfo,
|
||||||
|
waveform = uploadInfo.waveform,
|
||||||
|
inReplyToEventId = inReplyToEventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is MediaUploadInfo.AnyFile -> {
|
||||||
|
sendFile(
|
||||||
|
file = uploadInfo.file,
|
||||||
|
fileInfo = uploadInfo.fileInfo,
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = formattedCaption,
|
||||||
|
inReplyToEventId = inReplyToEventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We handle the cancellations here manually, so we suppress the warning
|
||||||
|
@Suppress("RunCatchingNotAllowed")
|
||||||
|
return handler
|
||||||
|
.mapCatching { uploadHandler ->
|
||||||
|
Timber.d("Added ongoing upload job, total: ${ongoingUploadJobs.size + 1}")
|
||||||
|
ongoingUploadJobs[Job] = uploadHandler
|
||||||
|
uploadHandler.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getTimeline(): Result<Timeline> {
|
||||||
|
return when (timelineMode) {
|
||||||
|
is Timeline.Mode.Thread -> {
|
||||||
|
room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = timelineMode.threadRootId))
|
||||||
|
}
|
||||||
|
else -> Result.success(room.liveTimeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up any temporary files or resources used during the media processing.
|
||||||
|
*/
|
||||||
|
override fun cleanUp() = preProcessor.cleanUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mediaId(uri: Uri?): String = uri?.path.orEmpty().hash()
|
||||||
|
private fun mediaId(file: File): String = file.path.orEmpty().hash()
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.mediaupload.api
|
package io.element.android.libraries.mediaupload.impl
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
@@ -19,6 +19,9 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
|||||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
@@ -33,7 +36,7 @@ import org.robolectric.RobolectricTestRunner
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class MediaSenderTest {
|
class DefaultMediaSenderTest {
|
||||||
private val mediaOptimizationConfig = MediaOptimizationConfig(
|
private val mediaOptimizationConfig = MediaOptimizationConfig(
|
||||||
compressImages = true,
|
compressImages = true,
|
||||||
videoCompressionPreset = VideoCompressionPreset.STANDARD,
|
videoCompressionPreset = VideoCompressionPreset.STANDARD,
|
||||||
@@ -42,7 +45,7 @@ class MediaSenderTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `given an attachment when sending it the preprocessor always runs`() = runTest {
|
fun `given an attachment when sending it the preprocessor always runs`() = runTest {
|
||||||
val preProcessor = FakeMediaPreProcessor()
|
val preProcessor = FakeMediaPreProcessor()
|
||||||
val sender = createMediaSender(
|
val sender = createDefaultMediaSender(
|
||||||
preProcessor = preProcessor,
|
preProcessor = preProcessor,
|
||||||
room = FakeJoinedRoom(
|
room = FakeJoinedRoom(
|
||||||
liveTimeline = FakeTimeline().apply {
|
liveTimeline = FakeTimeline().apply {
|
||||||
@@ -77,7 +80,7 @@ class MediaSenderTest {
|
|||||||
sendImageLambda = sendImageResult
|
sendImageLambda = sendImageResult
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val sender = createMediaSender(room = room)
|
val sender = createDefaultMediaSender(room = room)
|
||||||
|
|
||||||
val uri = Uri.parse("content://image.jpg")
|
val uri = Uri.parse("content://image.jpg")
|
||||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
||||||
@@ -88,7 +91,7 @@ class MediaSenderTest {
|
|||||||
val preProcessor = FakeMediaPreProcessor().apply {
|
val preProcessor = FakeMediaPreProcessor().apply {
|
||||||
givenResult(Result.failure(Exception()))
|
givenResult(Result.failure(Exception()))
|
||||||
}
|
}
|
||||||
val sender = createMediaSender(preProcessor)
|
val sender = createDefaultMediaSender(preProcessor)
|
||||||
|
|
||||||
val uri = Uri.parse("content://image.jpg")
|
val uri = Uri.parse("content://image.jpg")
|
||||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
||||||
@@ -110,7 +113,7 @@ class MediaSenderTest {
|
|||||||
sendImageLambda = sendImageResult
|
sendImageLambda = sendImageResult
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val sender = createMediaSender(
|
val sender = createDefaultMediaSender(
|
||||||
preProcessor = preProcessor,
|
preProcessor = preProcessor,
|
||||||
room = room,
|
room = room,
|
||||||
)
|
)
|
||||||
@@ -133,7 +136,7 @@ class MediaSenderTest {
|
|||||||
sendFileLambda = sendFileResult
|
sendFileLambda = sendFileResult
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val sender = createMediaSender(room = room)
|
val sender = createDefaultMediaSender(room = room)
|
||||||
val sendJob = launch {
|
val sendJob = launch {
|
||||||
val uri = Uri.parse("content://image.jpg")
|
val uri = Uri.parse("content://image.jpg")
|
||||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
||||||
@@ -155,11 +158,11 @@ class MediaSenderTest {
|
|||||||
sendFileResult.assertions().isCalledOnce()
|
sendFileResult.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMediaSender(
|
private fun createDefaultMediaSender(
|
||||||
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
room: JoinedRoom = FakeJoinedRoom(),
|
room: JoinedRoom = FakeJoinedRoom(),
|
||||||
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = MediaOptimizationConfigProvider { mediaOptimizationConfig },
|
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = MediaOptimizationConfigProvider { mediaOptimizationConfig },
|
||||||
) = MediaSender(
|
) = DefaultMediaSender(
|
||||||
preProcessor = preProcessor,
|
preProcessor = preProcessor,
|
||||||
room = room,
|
room = room,
|
||||||
timelineMode = Timeline.Mode.Live,
|
timelineMode = Timeline.Mode.Live,
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 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.test
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakeMediaSender(
|
||||||
|
private val preProcessMediaResult: () -> Result<MediaUploadInfo> = { lambdaError() },
|
||||||
|
private val sendPreProcessedMediaResult: () -> Result<Unit> = { lambdaError() },
|
||||||
|
private val sendMediaResult: () -> Result<Unit> = { lambdaError() },
|
||||||
|
private val sendVoiceMessageResult: () -> Result<Unit> = { lambdaError() },
|
||||||
|
private val cleanUpResult: () -> Unit = { lambdaError() },
|
||||||
|
) : MediaSender {
|
||||||
|
override suspend fun preProcessMedia(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||||
|
): Result<MediaUploadInfo> {
|
||||||
|
return preProcessMediaResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendPreProcessedMedia(
|
||||||
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
inReplyToEventId: EventId?,
|
||||||
|
): Result<Unit> {
|
||||||
|
return sendPreProcessedMediaResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendMedia(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
inReplyToEventId: EventId?,
|
||||||
|
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||||
|
): Result<Unit> {
|
||||||
|
return sendMediaResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendVoiceMessage(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
waveForm: List<Float>,
|
||||||
|
inReplyToEventId: EventId?,
|
||||||
|
): Result<Unit> {
|
||||||
|
return sendVoiceMessageResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanUp() {
|
||||||
|
cleanUpResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,7 @@ dependencies {
|
|||||||
testImplementation(projects.features.enterprise.test)
|
testImplementation(projects.features.enterprise.test)
|
||||||
testImplementation(projects.features.lockscreen.test)
|
testImplementation(projects.features.lockscreen.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
|
testImplementation(projects.services.appnavstate.impl)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.services.toolbox.impl)
|
testImplementation(projects.services.toolbox.impl)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived
|
|||||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
|
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||||
@@ -482,7 +483,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||||||
onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(),
|
onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(),
|
||||||
stringProvider: StringProvider = FakeStringProvider(),
|
stringProvider: StringProvider = FakeStringProvider(),
|
||||||
replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(),
|
replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(),
|
||||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
|
||||||
): NotificationBroadcastReceiverHandler {
|
): NotificationBroadcastReceiverHandler {
|
||||||
return NotificationBroadcastReceiverHandler(
|
return NotificationBroadcastReceiverHandler(
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import extension.setupDependencyInjection
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
* Copyright 2025 New Vector Ltd.
|
* Copyright 2025 New Vector Ltd.
|
||||||
@@ -16,8 +14,6 @@ android {
|
|||||||
namespace = "io.element.android.libraries.recentemojis.api"
|
namespace = "io.element.android.libraries.recentemojis.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|||||||
@@ -8,17 +8,6 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.recentemojis.api
|
package io.element.android.libraries.recentemojis.api
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
fun interface AddRecentEmoji {
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
suspend operator fun invoke(emoji: String): Result<Unit>
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
class AddRecentEmoji(
|
|
||||||
private val client: MatrixClient,
|
|
||||||
private val dispatchers: CoroutineDispatchers,
|
|
||||||
) {
|
|
||||||
suspend operator fun invoke(emoji: String): Result<Unit> = withContext(dispatchers.io) {
|
|
||||||
client.addRecentEmoji(emoji)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
* Copyright 2025 New Vector 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.recentemojis.impl
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.recentemojis.api.AddRecentEmoji
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultAddRecentEmoji(
|
||||||
|
private val client: MatrixClient,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
) : AddRecentEmoji {
|
||||||
|
override suspend operator fun invoke(emoji: String): Result<Unit> = withContext(dispatchers.io) {
|
||||||
|
client.addRecentEmoji(emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
|
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
|
||||||
|
import io.element.android.libraries.matrix.test.mxc.FakeMxcTools
|
||||||
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
|
||||||
@@ -131,7 +131,7 @@ private fun createDefaultVoiceMessageMediaRepo(
|
|||||||
mxcUri: String = MXC_URI,
|
mxcUri: String = MXC_URI,
|
||||||
) = DefaultVoiceMessageMediaRepo(
|
) = DefaultVoiceMessageMediaRepo(
|
||||||
cacheDir = temporaryFolder.root,
|
cacheDir = temporaryFolder.root,
|
||||||
mxcTools = MxcTools(),
|
mxcTools = FakeMxcTools(),
|
||||||
matrixMediaLoader = matrixMediaLoader,
|
matrixMediaLoader = matrixMediaLoader,
|
||||||
mediaSource = MediaSource(
|
mediaSource = MediaSource(
|
||||||
url = mxcUri,
|
url = mxcUri,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import extension.setupDependencyInjection
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
* Copyright 2025 New Vector Ltd.
|
* Copyright 2025 New Vector Ltd.
|
||||||
@@ -15,8 +13,6 @@ android {
|
|||||||
namespace = "io.element.android.libraries.workmanager.api"
|
namespace = "io.element.android.libraries.workmanager.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(libs.androidx.workmanager.runtime)
|
api(libs.androidx.workmanager.runtime)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ import org.gradle.plugin.use.PluginDependency
|
|||||||
fun Project.setupDependencyInjection(
|
fun Project.setupDependencyInjection(
|
||||||
generateNodeFactories: Boolean = shouldApplyAppyxCodegen(),
|
generateNodeFactories: Boolean = shouldApplyAppyxCodegen(),
|
||||||
) {
|
) {
|
||||||
|
if (project.path.endsWith(":api")) {
|
||||||
|
error("api module should not use setupDependencyInjection(). Move the implementation to `:impl` module")
|
||||||
|
}
|
||||||
|
|
||||||
val libs = the<LibrariesForLibs>()
|
val libs = the<LibrariesForLibs>()
|
||||||
|
|
||||||
// Apply Metro plugin and configure it
|
// Apply Metro plugin and configure it
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import extension.setupDependencyInjection
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
* Copyright 2022-2024 New Vector Ltd.
|
* Copyright 2022-2024 New Vector Ltd.
|
||||||
@@ -16,8 +14,6 @@ android {
|
|||||||
namespace = "io.element.android.services.appnavstate.api"
|
namespace = "io.element.android.services.appnavstate.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
implementation(libs.androidx.lifecycle.runtime)
|
implementation(libs.androidx.lifecycle.runtime)
|
||||||
|
|||||||
@@ -8,63 +8,36 @@
|
|||||||
|
|
||||||
package io.element.android.services.appnavstate.api
|
package io.element.android.services.appnavstate.api
|
||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
|
||||||
import dev.zacsweers.metro.Inject
|
|
||||||
import dev.zacsweers.metro.SingleIn
|
|
||||||
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.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the active rooms for a given session so they can be reused instead of instantiating new ones.
|
* Holds the active rooms for a given session so they can be reused instead of instantiating new ones.
|
||||||
*/
|
*/
|
||||||
@SingleIn(AppScope::class)
|
interface ActiveRoomsHolder {
|
||||||
@Inject
|
|
||||||
class ActiveRoomsHolder {
|
|
||||||
private val rooms = ConcurrentHashMap<SessionId, MutableSet<JoinedRoom>>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new held room for the given sessionId.
|
* Adds a new held room for the given sessionId.
|
||||||
*/
|
*/
|
||||||
fun addRoom(room: JoinedRoom) {
|
fun addRoom(room: JoinedRoom)
|
||||||
val roomsForSessionId = rooms.getOrPut(key = room.sessionId, defaultValue = { mutableSetOf() })
|
|
||||||
if (roomsForSessionId.none { it.roomId == room.roomId }) {
|
|
||||||
// We don't want to add the same room multiple times
|
|
||||||
roomsForSessionId.add(room)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the last room added for the given [sessionId] or null if no room was added.
|
* Returns the last room added for the given [sessionId] or null if no room was added.
|
||||||
*/
|
*/
|
||||||
fun getActiveRoom(sessionId: SessionId): JoinedRoom? {
|
fun getActiveRoom(sessionId: SessionId): JoinedRoom?
|
||||||
return rooms[sessionId]?.lastOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an active room associated to the given [sessionId], with the given [roomId], or null if none match.
|
* Returns an active room associated to the given [sessionId], with the given [roomId], or null if none match.
|
||||||
*/
|
*/
|
||||||
fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom? {
|
fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom?
|
||||||
return rooms[sessionId]?.find { it.roomId == roomId }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes any room matching the provided [sessionId] and [roomId].
|
* Removes any room matching the provided [sessionId] and [roomId].
|
||||||
*/
|
*/
|
||||||
fun removeRoom(sessionId: SessionId, roomId: RoomId) {
|
fun removeRoom(sessionId: SessionId, roomId: RoomId)
|
||||||
val roomsForSessionId = rooms[sessionId] ?: return
|
|
||||||
roomsForSessionId.removeIf { it.roomId == roomId }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all the rooms for the given sessionId.
|
* Clears all the rooms for the given sessionId.
|
||||||
*/
|
*/
|
||||||
fun clear(sessionId: SessionId) {
|
fun clear(sessionId: SessionId)
|
||||||
val activeRooms = rooms.remove(sessionId) ?: return
|
|
||||||
for (room in activeRooms) {
|
|
||||||
// Destroy the room to reset the live timelines
|
|
||||||
room.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
* Copyright 2025 New Vector 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.services.appnavstate.impl
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
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.room.JoinedRoom
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultActiveRoomsHolder : ActiveRoomsHolder {
|
||||||
|
private val rooms = ConcurrentHashMap<SessionId, MutableSet<JoinedRoom>>()
|
||||||
|
|
||||||
|
override fun addRoom(room: JoinedRoom) {
|
||||||
|
val roomsForSessionId = rooms.getOrPut(key = room.sessionId, defaultValue = { mutableSetOf() })
|
||||||
|
if (roomsForSessionId.none { it.roomId == room.roomId }) {
|
||||||
|
// We don't want to add the same room multiple times
|
||||||
|
roomsForSessionId.add(room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getActiveRoom(sessionId: SessionId): JoinedRoom? {
|
||||||
|
return rooms[sessionId]?.lastOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom? {
|
||||||
|
return rooms[sessionId]?.find { it.roomId == roomId }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeRoom(sessionId: SessionId, roomId: RoomId) {
|
||||||
|
val roomsForSessionId = rooms[sessionId] ?: return
|
||||||
|
roomsForSessionId.removeIf { it.roomId == roomId }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear(sessionId: SessionId) {
|
||||||
|
val activeRooms = rooms.remove(sessionId) ?: return
|
||||||
|
for (room in activeRooms) {
|
||||||
|
// Destroy the room to reset the live timelines
|
||||||
|
room.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user