Merge pull request #3744 from element-hq/feature/bma/resizeMedia
Add setting to compress image and video
This commit is contained in:
commit
870ae5fb54
39 changed files with 198 additions and 55 deletions
|
|
@ -15,5 +15,5 @@ import kotlinx.parcelize.Parcelize
|
|||
@Immutable
|
||||
sealed interface Attachment : Parcelable {
|
||||
@Parcelize
|
||||
data class Media(val localMedia: LocalMedia, val compressIfPossible: Boolean) : Attachment
|
||||
data class Media(val localMedia: LocalMedia) : Attachment
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
mediaSender.sendMedia(
|
||||
uri = mediaAttachment.localMedia.uri,
|
||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||
compressIfPossible = mediaAttachment.compressIfPossible,
|
||||
progressCallback = progressCallback
|
||||
).getOrThrow()
|
||||
}.fold(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ fun anAttachmentsPreviewState(
|
|||
) = AttachmentsPreviewState(
|
||||
attachment = Attachment.Media(
|
||||
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
||||
compressIfPossible = true
|
||||
),
|
||||
sendActionState = sendActionState,
|
||||
eventSink = {}
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
handlePickedMedia(attachmentsState, uri, mimeType)
|
||||
}
|
||||
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri ->
|
||||
handlePickedMedia(attachmentsState, uri, compressIfPossible = false)
|
||||
handlePickedMedia(attachmentsState, uri)
|
||||
}
|
||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
|
||||
handlePickedMedia(attachmentsState, uri, MimeTypes.IMAGE_JPEG)
|
||||
|
|
@ -294,7 +294,6 @@ class MessageComposerPresenter @Inject constructor(
|
|||
name = null,
|
||||
formattedFileSize = null
|
||||
),
|
||||
compressIfPossible = true
|
||||
),
|
||||
attachmentState = attachmentsState,
|
||||
)
|
||||
|
|
@ -493,7 +492,6 @@ class MessageComposerPresenter @Inject constructor(
|
|||
attachmentsState: MutableState<AttachmentsState>,
|
||||
uri: Uri?,
|
||||
mimeType: String? = null,
|
||||
compressIfPossible: Boolean = true,
|
||||
) {
|
||||
if (uri == null) {
|
||||
attachmentsState.value = AttachmentsState.None
|
||||
|
|
@ -505,7 +503,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
name = null,
|
||||
formattedFileSize = null
|
||||
)
|
||||
val mediaAttachment = Attachment.Media(localMedia, compressIfPossible)
|
||||
val mediaAttachment = Attachment.Media(localMedia)
|
||||
val isPreviewable = when {
|
||||
MimeTypes.isImage(localMedia.info.mimeType) -> true
|
||||
MimeTypes.isVideo(localMedia.info.mimeType) -> true
|
||||
|
|
@ -535,7 +533,6 @@ class MessageComposerPresenter @Inject constructor(
|
|||
mediaSender.sendMedia(
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
compressIfPossible = false,
|
||||
progressCallback = progressCallback
|
||||
).getOrThrow()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
|
||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
|
||||
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
||||
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||
|
|
@ -26,6 +27,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender
|
|||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.mockk.mockk
|
||||
|
|
@ -120,8 +122,8 @@ class AttachmentsPreviewPresenterTest {
|
|||
room: MatrixRoom = FakeMatrixRoom()
|
||||
): AttachmentsPreviewPresenter {
|
||||
return AttachmentsPreviewPresenter(
|
||||
attachment = Attachment.Media(localMedia, compressIfPossible = false),
|
||||
mediaSender = MediaSender(mediaPreProcessor, room)
|
||||
attachment = aMediaAttachment(localMedia),
|
||||
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.fixtures
|
|||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
|
||||
fun aMediaAttachment(localMedia: LocalMedia, compressIfPossible: Boolean = true) = Attachment.Media(
|
||||
fun aMediaAttachment(localMedia: LocalMedia) = Attachment.Media(
|
||||
localMedia = localMedia,
|
||||
compressIfPossible = compressIfPossible,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1489,7 +1489,7 @@ class MessageComposerPresenterTest {
|
|||
featureFlagService,
|
||||
sessionPreferencesStore,
|
||||
localMediaFactory,
|
||||
MediaSender(mediaPreProcessor, room),
|
||||
MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
|
||||
snackbarDispatcher,
|
||||
analyticsService,
|
||||
DefaultMessageComposerContext(),
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
|
|||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
|
|
@ -61,7 +62,7 @@ class VoiceMessageComposerPresenterTest {
|
|||
sendMediaResult = sendMediaResult
|
||||
)
|
||||
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
|
||||
private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom)
|
||||
private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom, InMemorySessionPreferencesStore())
|
||||
private val messageComposerContext = FakeMessageComposerContext()
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ dependencies {
|
|||
implementation(projects.features.deactivation.api)
|
||||
implementation(projects.features.roomlist.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.services.analytics.compose)
|
||||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.coil.compose)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
|
|||
sealed interface AdvancedSettingsEvents {
|
||||
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvents
|
||||
data object ChangeTheme : AdvancedSettingsEvents
|
||||
data object CancelChangeTheme : AdvancedSettingsEvents
|
||||
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ class AdvancedSettingsPresenter @Inject constructor(
|
|||
val isSharePresenceEnabled by sessionPreferencesStore
|
||||
.isSharePresenceEnabled()
|
||||
.collectAsState(initial = true)
|
||||
val doesCompressMedia by sessionPreferencesStore
|
||||
.doesCompressMedia()
|
||||
.collectAsState(initial = false)
|
||||
val theme by remember {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
|
|
@ -49,6 +52,9 @@ class AdvancedSettingsPresenter @Inject constructor(
|
|||
is AdvancedSettingsEvents.SetSharePresenceEnabled -> localCoroutineScope.launch {
|
||||
sessionPreferencesStore.setSharePresence(event.enabled)
|
||||
}
|
||||
is AdvancedSettingsEvents.SetCompressMedia -> localCoroutineScope.launch {
|
||||
sessionPreferencesStore.setCompressMedia(event.compress)
|
||||
}
|
||||
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
|
||||
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
|
||||
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
||||
|
|
@ -61,6 +67,7 @@ class AdvancedSettingsPresenter @Inject constructor(
|
|||
return AdvancedSettingsState(
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSharePresenceEnabled = isSharePresenceEnabled,
|
||||
doesCompressMedia = doesCompressMedia,
|
||||
theme = theme,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = { handleEvents(it) }
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
|
|||
data class AdvancedSettingsState(
|
||||
val isDeveloperModeEnabled: Boolean,
|
||||
val isSharePresenceEnabled: Boolean,
|
||||
val doesCompressMedia: Boolean,
|
||||
val theme: Theme,
|
||||
val showChangeThemeDialog: Boolean,
|
||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||
|
|
|
|||
|
|
@ -16,18 +16,21 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
|
|||
aAdvancedSettingsState(),
|
||||
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
||||
aAdvancedSettingsState(showChangeThemeDialog = true),
|
||||
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
|
||||
aAdvancedSettingsState(isSharePresenceEnabled = true),
|
||||
aAdvancedSettingsState(doesCompressMedia = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aAdvancedSettingsState(
|
||||
isDeveloperModeEnabled: Boolean = false,
|
||||
isSendPublicReadReceiptsEnabled: Boolean = false,
|
||||
isSharePresenceEnabled: Boolean = false,
|
||||
doesCompressMedia: Boolean = false,
|
||||
showChangeThemeDialog: Boolean = false,
|
||||
eventSink: (AdvancedSettingsEvents) -> Unit = {},
|
||||
) = AdvancedSettingsState(
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSharePresenceEnabled = isSendPublicReadReceiptsEnabled,
|
||||
isSharePresenceEnabled = isSharePresenceEnabled,
|
||||
doesCompressMedia = doesCompressMedia,
|
||||
theme = Theme.System,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = eventSink
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.theme.themes
|
||||
import io.element.android.features.preferences.impl.R
|
||||
|
|
@ -23,6 +24,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
@ -32,6 +35,7 @@ fun AdvancedSettingsView(
|
|||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val analyticsService = LocalAnalyticsService.current
|
||||
PreferencePage(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
|
|
@ -72,6 +76,28 @@ fun AdvancedSettingsView(
|
|||
),
|
||||
onClick = { state.eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) }
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_title))
|
||||
},
|
||||
supportingContent = {
|
||||
Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_description))
|
||||
},
|
||||
trailingContent = ListItemContent.Switch(
|
||||
checked = state.doesCompressMedia,
|
||||
),
|
||||
onClick = {
|
||||
val newValue = !state.doesCompressMedia
|
||||
analyticsService.captureInteraction(
|
||||
if (newValue) {
|
||||
Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
|
||||
} else {
|
||||
Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
|
||||
}
|
||||
)
|
||||
state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (state.showChangeThemeDialog) {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class AdvancedSettingsPresenterTest {
|
|||
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
||||
assertThat(initialState.showChangeThemeDialog).isFalse()
|
||||
assertThat(initialState.isSharePresenceEnabled).isTrue()
|
||||
assertThat(initialState.doesCompressMedia).isFalse()
|
||||
assertThat(initialState.theme).isEqualTo(Theme.System)
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +69,21 @@ class AdvancedSettingsPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compress media off on`() = runTest {
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
assertThat(initialState.doesCompressMedia).isFalse()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true))
|
||||
assertThat(awaitItem().doesCompressMedia).isTrue()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(false))
|
||||
assertThat(awaitItem().doesCompressMedia).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - change theme`() = runTest {
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
|
|
|
|||
|
|
@ -8,12 +8,18 @@
|
|||
package io.element.android.features.preferences.impl.advanced
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
|
|
@ -91,16 +97,64 @@ class AdvancedSettingsViewTest {
|
|||
rule.clickOn(R.string.screen_advanced_settings_share_presence)
|
||||
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on media to enable compression emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
rule.setAdvancedSettingsView(
|
||||
state = aAdvancedSettingsState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
analyticsService = analyticsService
|
||||
)
|
||||
rule.clickOn(R.string.screen_advanced_settings_media_compression_description)
|
||||
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true))
|
||||
assertThat(analyticsService.capturedEvents).isEqualTo(
|
||||
listOf(
|
||||
Interaction(
|
||||
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on media to disable compression emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
rule.setAdvancedSettingsView(
|
||||
state = aAdvancedSettingsState(
|
||||
doesCompressMedia = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
analyticsService = analyticsService
|
||||
)
|
||||
rule.clickOn(R.string.screen_advanced_settings_media_compression_description)
|
||||
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false))
|
||||
assertThat(analyticsService.capturedEvents).isEqualTo(
|
||||
listOf(
|
||||
Interaction(
|
||||
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAdvancedSettingsView(
|
||||
state: AdvancedSettingsState,
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
AdvancedSettingsView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalAnalyticsService provides analyticsService,
|
||||
) {
|
||||
AdvancedSettingsView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ dependencies {
|
|||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ class SharePresenter @AssistedInject constructor(
|
|||
private val shareIntentHandler: ShareIntentHandler,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val mediaPreProcessor: MediaPreProcessor,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
) : Presenter<ShareState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -71,13 +73,16 @@ class SharePresenter @AssistedInject constructor(
|
|||
roomIds
|
||||
.map { roomId ->
|
||||
val room = matrixClient.getRoom(roomId) ?: return@map false
|
||||
val mediaSender = MediaSender(preProcessor = mediaPreProcessor, room = room)
|
||||
val mediaSender = MediaSender(
|
||||
preProcessor = mediaPreProcessor,
|
||||
room = room,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
filesToShare
|
||||
.map { fileToShare ->
|
||||
mediaSender.sendMedia(
|
||||
uri = fileToShare.uri,
|
||||
mimeType = fileToShare.mimeType,
|
||||
compressIfPossible = true,
|
||||
).isSuccess
|
||||
}
|
||||
.all { it }
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
|||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -154,7 +155,8 @@ class SharePresenterTest {
|
|||
appCoroutineScope = this,
|
||||
shareIntentHandler = shareIntentHandler,
|
||||
matrixClient = matrixClient,
|
||||
mediaPreProcessor = mediaPreProcessor
|
||||
mediaPreProcessor = mediaPreProcessor,
|
||||
InMemorySessionPreferencesStore(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
|
|||
posthog = "com.posthog:posthog-android:3.8.2"
|
||||
sentry = "io.sentry:sentry-android:7.16.0"
|
||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.27.0"
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
||||
|
||||
# Emojibase
|
||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.3.3"
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
api(projects.libraries.matrix.api)
|
||||
api(projects.libraries.preferences.api)
|
||||
implementation(libs.inject)
|
||||
implementation(libs.coroutines.core)
|
||||
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.test.junit)
|
||||
|
|
|
|||
|
|
@ -12,14 +12,17 @@ import io.element.android.libraries.core.extensions.flatMapCatching
|
|||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
class MediaSender @Inject constructor(
|
||||
private val preProcessor: MediaPreProcessor,
|
||||
private val room: MatrixRoom,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
) {
|
||||
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
||||
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
||||
|
|
@ -27,11 +30,11 @@ class MediaSender @Inject constructor(
|
|||
suspend fun sendMedia(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
compressIfPossible: Boolean,
|
||||
caption: String? = null,
|
||||
formattedCaption: String? = null,
|
||||
progressCallback: ProgressCallback? = null
|
||||
): Result<Unit> {
|
||||
val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
|
||||
return preProcessor
|
||||
.process(
|
||||
uri = uri,
|
||||
|
|
@ -49,6 +52,7 @@ class MediaSender @Inject constructor(
|
|||
}
|
||||
.handleSendResult()
|
||||
}
|
||||
|
||||
suspend fun sendVoiceMessage(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
|
|
@ -60,7 +64,7 @@ class MediaSender @Inject constructor(
|
|||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = true,
|
||||
compressIfPossible = false
|
||||
compressIfPossible = false,
|
||||
)
|
||||
.flatMapCatching { info ->
|
||||
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -33,7 +35,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(preProcessor)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
|
||||
assertThat(preProcessor.processCallCount).isEqualTo(1)
|
||||
}
|
||||
|
|
@ -49,7 +51,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(room = room)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
sendMediaResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +63,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(preProcessor)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
|
||||
assertThat(result.exceptionOrNull()).isNotNull()
|
||||
}
|
||||
|
|
@ -74,7 +76,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(room = room)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
|
||||
assertThat(result.exceptionOrNull()).isNotNull()
|
||||
}
|
||||
|
|
@ -88,7 +90,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(room = room)
|
||||
val sendJob = launch {
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
}
|
||||
// Wait until several internal tasks run and the file is being uploaded
|
||||
advanceTimeBy(3L)
|
||||
|
|
@ -109,8 +111,10 @@ class MediaSenderTest {
|
|||
private fun aMediaSender(
|
||||
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||
room: MatrixRoom = FakeMatrixRoom(),
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
) = MediaSender(
|
||||
preProcessor,
|
||||
room,
|
||||
preProcessor = preProcessor,
|
||||
room = room,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ImageCompressor @Inject constructor(
|
|||
resizeMode: ResizeMode,
|
||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
||||
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||
desiredQuality: Int = 80,
|
||||
desiredQuality: Int = 78,
|
||||
): Result<ImageCompressionResult> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow()
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class VideoCompressor @Inject constructor(
|
|||
val future = Transcoder.into(tmpFile.path)
|
||||
.setVideoTrackStrategy(
|
||||
DefaultVideoStrategy.Builder()
|
||||
.addResizer(AtMostResizer(1920, 1080))
|
||||
.addResizer(AtMostResizer(720, 480))
|
||||
.build()
|
||||
)
|
||||
.addDataSource(context, uri)
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ class AndroidMediaPreProcessorTest {
|
|||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 114_867,
|
||||
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4567),
|
||||
size = 109_908,
|
||||
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4484),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K13]7q%zWC00R4of%\$baad"
|
||||
)
|
||||
|
|
@ -84,7 +84,7 @@ class AndroidMediaPreProcessorTest {
|
|||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 114_867,
|
||||
size = 109_908,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
|
|
|
|||
|
|
@ -28,5 +28,8 @@ interface SessionPreferencesStore {
|
|||
suspend fun setSkipSessionVerification(skip: Boolean)
|
||||
fun isSessionVerificationSkipped(): Flow<Boolean>
|
||||
|
||||
suspend fun setCompressMedia(compress: Boolean)
|
||||
fun doesCompressMedia(): Flow<Boolean>
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class DefaultSessionPreferencesStore(
|
|||
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
|
||||
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
|
||||
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
|
||||
private val compressMedia = booleanPreferencesKey("compressMedia")
|
||||
|
||||
private val dataStoreFile = storeFile(context, sessionId)
|
||||
private val store = PreferenceDataStoreFactory.create(
|
||||
|
|
@ -81,6 +82,9 @@ class DefaultSessionPreferencesStore(
|
|||
override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
|
||||
override fun isSessionVerificationSkipped(): Flow<Boolean> = get(skipSessionVerification) { false }
|
||||
|
||||
override suspend fun setCompressMedia(compress: Boolean) = update(compressMedia, compress)
|
||||
override fun doesCompressMedia(): Flow<Boolean> = get(compressMedia) { false }
|
||||
|
||||
override suspend fun clear() {
|
||||
dataStoreFile.safeDelete()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class InMemorySessionPreferencesStore(
|
|||
isSendTypingNotificationsEnabled: Boolean = true,
|
||||
isRenderTypingNotificationsEnabled: Boolean = true,
|
||||
isSessionVerificationSkipped: Boolean = false,
|
||||
doesCompressMedia: Boolean = false,
|
||||
) : SessionPreferencesStore {
|
||||
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
|
||||
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
||||
|
|
@ -25,6 +26,7 @@ class InMemorySessionPreferencesStore(
|
|||
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
|
||||
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
|
||||
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
|
||||
private val doesCompressMedia = MutableStateFlow(doesCompressMedia)
|
||||
var clearCallCount = 0
|
||||
private set
|
||||
|
||||
|
|
@ -66,6 +68,10 @@ class InMemorySessionPreferencesStore(
|
|||
return isSessionVerificationSkipped
|
||||
}
|
||||
|
||||
override suspend fun setCompressMedia(compress: Boolean) = doesCompressMedia.emit(compress)
|
||||
|
||||
override fun doesCompressMedia(): Flow<Boolean> = doesCompressMedia
|
||||
|
||||
override suspend fun clear() {
|
||||
clearCallCount++
|
||||
isSendPublicReadReceiptsEnabled.tryEmit(true)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0c6ec8d30f2fd12c78bf62c9913a68c7a1f6328f0ac9e577317d0abf72e1131c
|
||||
size 41788
|
||||
oid sha256:13e7793d8dd6d08e182b128a9b3dac2877557e8bdd220561d36c2ce1650b94ff
|
||||
size 48107
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e37410e8e25d6908bbd88db983be7f1c50b4f848763b66c80bf18de97d7f4916
|
||||
size 41548
|
||||
oid sha256:1bac5247c3a4990eb9155a21f72a49059cbaa93288380ba1ab6be2def8b3a6a9
|
||||
size 47876
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4d64cb8ce98dfdf2345e12596e4eb3777ea4cde2bd36303d0af6b47614e28f3f
|
||||
size 31044
|
||||
oid sha256:11c969c1d04150cef68da64865bade2ea3bfc1aa5f0d790262315131b57d6233
|
||||
size 31702
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:69308b3b8b6a7b8a0c032ba6b023a49fcfbb4f05317cbe96e690b4aa3c6992b8
|
||||
size 41621
|
||||
oid sha256:dc1aa9348e470e9d7e7e1e838e18a3403159c2effb49fa359b2b2db97dd81961
|
||||
size 47901
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90d1184879c5a34dc27cc942ec3110e14ccd9a90c152b10a1844fde0566d54fe
|
||||
size 47841
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:702a4067afb752978fef59ca45b6d4c871d084953ab19a3d845f0018a894ac6a
|
||||
size 40510
|
||||
oid sha256:c2beb4f4f190f6aca2d4da338d980e8611283ffb9bfd2f402482f8ecc629cf22
|
||||
size 46759
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1904587ad63098e18e17663a3bac34f2ad24d79967ab44789a1bc1b994805eeb
|
||||
size 40205
|
||||
oid sha256:b65acfd3126efc5cd4fe1a929a786468cc18ee371cb4462ef0cf9f6e8963fcea
|
||||
size 46456
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:14098a8dc6d7c9d99b6a04710e1385b39839bf18be3c9ddae7ce437a0c1bf64b
|
||||
size 28729
|
||||
oid sha256:8e90bbde9aac7e710703e836f293a00b2a5f35447d4d63c9732de21a0f291449
|
||||
size 29336
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6a6151f04f2dca8124597c6d144cafe15a51f7fe6638cfed4380a9b6e125bed4
|
||||
size 40259
|
||||
oid sha256:6da0cf1729162fa1745bcb4dd86be06f90d3bcf6bf034e4a64e1ee9119b6cdd5
|
||||
size 46501
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66109868b1e893bc569d70110e3e587d7a17d777838cfc3e5c3d3189338d924e
|
||||
size 46423
|
||||
Loading…
Add table
Add a link
Reference in a new issue