Add media upload setting.
Compress media regarding the settings. Image compression change quality to 78% Video compression change size to 720 x 48
This commit is contained in:
parent
932b189cc4
commit
2531ccf8e9
22 changed files with 162 additions and 29 deletions
|
|
@ -15,5 +15,5 @@ import kotlinx.parcelize.Parcelize
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface Attachment : Parcelable {
|
sealed interface Attachment : Parcelable {
|
||||||
@Parcelize
|
@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(
|
mediaSender.sendMedia(
|
||||||
uri = mediaAttachment.localMedia.uri,
|
uri = mediaAttachment.localMedia.uri,
|
||||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||||
compressIfPossible = mediaAttachment.compressIfPossible,
|
|
||||||
progressCallback = progressCallback
|
progressCallback = progressCallback
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
}.fold(
|
}.fold(
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ fun anAttachmentsPreviewState(
|
||||||
) = AttachmentsPreviewState(
|
) = AttachmentsPreviewState(
|
||||||
attachment = Attachment.Media(
|
attachment = Attachment.Media(
|
||||||
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
||||||
compressIfPossible = true
|
|
||||||
),
|
),
|
||||||
sendActionState = sendActionState,
|
sendActionState = sendActionState,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
handlePickedMedia(attachmentsState, uri, mimeType)
|
handlePickedMedia(attachmentsState, uri, mimeType)
|
||||||
}
|
}
|
||||||
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri ->
|
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri ->
|
||||||
handlePickedMedia(attachmentsState, uri, compressIfPossible = false)
|
handlePickedMedia(attachmentsState, uri)
|
||||||
}
|
}
|
||||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
|
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
|
||||||
handlePickedMedia(attachmentsState, uri, MimeTypes.IMAGE_JPEG)
|
handlePickedMedia(attachmentsState, uri, MimeTypes.IMAGE_JPEG)
|
||||||
|
|
@ -294,7 +294,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
name = null,
|
name = null,
|
||||||
formattedFileSize = null
|
formattedFileSize = null
|
||||||
),
|
),
|
||||||
compressIfPossible = true
|
|
||||||
),
|
),
|
||||||
attachmentState = attachmentsState,
|
attachmentState = attachmentsState,
|
||||||
)
|
)
|
||||||
|
|
@ -493,7 +492,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
attachmentsState: MutableState<AttachmentsState>,
|
attachmentsState: MutableState<AttachmentsState>,
|
||||||
uri: Uri?,
|
uri: Uri?,
|
||||||
mimeType: String? = null,
|
mimeType: String? = null,
|
||||||
compressIfPossible: Boolean = true,
|
|
||||||
) {
|
) {
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
attachmentsState.value = AttachmentsState.None
|
attachmentsState.value = AttachmentsState.None
|
||||||
|
|
@ -505,7 +503,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
name = null,
|
name = null,
|
||||||
formattedFileSize = null
|
formattedFileSize = null
|
||||||
)
|
)
|
||||||
val mediaAttachment = Attachment.Media(localMedia, compressIfPossible)
|
val mediaAttachment = Attachment.Media(localMedia)
|
||||||
val isPreviewable = when {
|
val isPreviewable = when {
|
||||||
MimeTypes.isImage(localMedia.info.mimeType) -> true
|
MimeTypes.isImage(localMedia.info.mimeType) -> true
|
||||||
MimeTypes.isVideo(localMedia.info.mimeType) -> true
|
MimeTypes.isVideo(localMedia.info.mimeType) -> true
|
||||||
|
|
@ -535,7 +533,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
mediaSender.sendMedia(
|
mediaSender.sendMedia(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
compressIfPossible = false,
|
|
||||||
progressCallback = progressCallback
|
progressCallback = progressCallback
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ dependencies {
|
||||||
implementation(projects.features.deactivation.api)
|
implementation(projects.features.deactivation.api)
|
||||||
implementation(projects.features.roomlist.api)
|
implementation(projects.features.roomlist.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
implementation(projects.services.analytics.compose)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
|
||||||
sealed interface AdvancedSettingsEvents {
|
sealed interface AdvancedSettingsEvents {
|
||||||
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||||
data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||||
|
data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvents
|
||||||
data object ChangeTheme : AdvancedSettingsEvents
|
data object ChangeTheme : AdvancedSettingsEvents
|
||||||
data object CancelChangeTheme : AdvancedSettingsEvents
|
data object CancelChangeTheme : AdvancedSettingsEvents
|
||||||
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||||
val isSharePresenceEnabled by sessionPreferencesStore
|
val isSharePresenceEnabled by sessionPreferencesStore
|
||||||
.isSharePresenceEnabled()
|
.isSharePresenceEnabled()
|
||||||
.collectAsState(initial = true)
|
.collectAsState(initial = true)
|
||||||
|
val doesCompressMedia by sessionPreferencesStore
|
||||||
|
.doesCompressMedia()
|
||||||
|
.collectAsState(initial = false)
|
||||||
val theme by remember {
|
val theme by remember {
|
||||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +52,9 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||||
is AdvancedSettingsEvents.SetSharePresenceEnabled -> localCoroutineScope.launch {
|
is AdvancedSettingsEvents.SetSharePresenceEnabled -> localCoroutineScope.launch {
|
||||||
sessionPreferencesStore.setSharePresence(event.enabled)
|
sessionPreferencesStore.setSharePresence(event.enabled)
|
||||||
}
|
}
|
||||||
|
is AdvancedSettingsEvents.SetCompressMedia -> localCoroutineScope.launch {
|
||||||
|
sessionPreferencesStore.setCompressMedia(event.compress)
|
||||||
|
}
|
||||||
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
|
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
|
||||||
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
|
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
|
||||||
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
||||||
|
|
@ -61,6 +67,7 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||||
return AdvancedSettingsState(
|
return AdvancedSettingsState(
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
isSharePresenceEnabled = isSharePresenceEnabled,
|
isSharePresenceEnabled = isSharePresenceEnabled,
|
||||||
|
doesCompressMedia = doesCompressMedia,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
showChangeThemeDialog = showChangeThemeDialog,
|
showChangeThemeDialog = showChangeThemeDialog,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
|
||||||
data class AdvancedSettingsState(
|
data class AdvancedSettingsState(
|
||||||
val isDeveloperModeEnabled: Boolean,
|
val isDeveloperModeEnabled: Boolean,
|
||||||
val isSharePresenceEnabled: Boolean,
|
val isSharePresenceEnabled: Boolean,
|
||||||
|
val doesCompressMedia: Boolean,
|
||||||
val theme: Theme,
|
val theme: Theme,
|
||||||
val showChangeThemeDialog: Boolean,
|
val showChangeThemeDialog: Boolean,
|
||||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,21 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
|
||||||
aAdvancedSettingsState(),
|
aAdvancedSettingsState(),
|
||||||
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
||||||
aAdvancedSettingsState(showChangeThemeDialog = true),
|
aAdvancedSettingsState(showChangeThemeDialog = true),
|
||||||
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
|
aAdvancedSettingsState(isSharePresenceEnabled = true),
|
||||||
|
aAdvancedSettingsState(doesCompressMedia = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aAdvancedSettingsState(
|
fun aAdvancedSettingsState(
|
||||||
isDeveloperModeEnabled: Boolean = false,
|
isDeveloperModeEnabled: Boolean = false,
|
||||||
isSendPublicReadReceiptsEnabled: Boolean = false,
|
isSharePresenceEnabled: Boolean = false,
|
||||||
|
doesCompressMedia: Boolean = false,
|
||||||
showChangeThemeDialog: Boolean = false,
|
showChangeThemeDialog: Boolean = false,
|
||||||
eventSink: (AdvancedSettingsEvents) -> Unit = {},
|
eventSink: (AdvancedSettingsEvents) -> Unit = {},
|
||||||
) = AdvancedSettingsState(
|
) = AdvancedSettingsState(
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
isSharePresenceEnabled = isSendPublicReadReceiptsEnabled,
|
isSharePresenceEnabled = isSharePresenceEnabled,
|
||||||
|
doesCompressMedia = doesCompressMedia,
|
||||||
theme = Theme.System,
|
theme = Theme.System,
|
||||||
showChangeThemeDialog = showChangeThemeDialog,
|
showChangeThemeDialog = showChangeThemeDialog,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
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.Theme
|
||||||
import io.element.android.compound.theme.themes
|
import io.element.android.compound.theme.themes
|
||||||
import io.element.android.features.preferences.impl.R
|
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.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
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.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
|
@ -32,6 +35,7 @@ fun AdvancedSettingsView(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val analyticsService = LocalAnalyticsService.current
|
||||||
PreferencePage(
|
PreferencePage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
|
|
@ -72,6 +76,28 @@ fun AdvancedSettingsView(
|
||||||
),
|
),
|
||||||
onClick = { state.eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) }
|
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) {
|
if (state.showChangeThemeDialog) {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class AdvancedSettingsPresenterTest {
|
||||||
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
||||||
assertThat(initialState.showChangeThemeDialog).isFalse()
|
assertThat(initialState.showChangeThemeDialog).isFalse()
|
||||||
assertThat(initialState.isSharePresenceEnabled).isTrue()
|
assertThat(initialState.isSharePresenceEnabled).isTrue()
|
||||||
|
assertThat(initialState.doesCompressMedia).isFalse()
|
||||||
assertThat(initialState.theme).isEqualTo(Theme.System)
|
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
|
@Test
|
||||||
fun `present - change theme`() = runTest {
|
fun `present - change theme`() = runTest {
|
||||||
val presenter = createAdvancedSettingsPresenter()
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,18 @@
|
||||||
package io.element.android.features.preferences.impl.advanced
|
package io.element.android.features.preferences.impl.advanced
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.compound.theme.Theme
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
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.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
|
@ -91,16 +97,64 @@ class AdvancedSettingsViewTest {
|
||||||
rule.clickOn(R.string.screen_advanced_settings_share_presence)
|
rule.clickOn(R.string.screen_advanced_settings_share_presence)
|
||||||
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true))
|
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(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAdvancedSettingsView(
|
||||||
state: AdvancedSettingsState,
|
state: AdvancedSettingsState,
|
||||||
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
AdvancedSettingsView(
|
CompositionLocalProvider(
|
||||||
state = state,
|
LocalAnalyticsService provides analyticsService,
|
||||||
onBackClick = onBackClick,
|
) {
|
||||||
)
|
AdvancedSettingsView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -31,6 +32,7 @@ class SharePresenter @AssistedInject constructor(
|
||||||
private val shareIntentHandler: ShareIntentHandler,
|
private val shareIntentHandler: ShareIntentHandler,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val mediaPreProcessor: MediaPreProcessor,
|
private val mediaPreProcessor: MediaPreProcessor,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : Presenter<ShareState> {
|
) : Presenter<ShareState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -71,13 +73,16 @@ class SharePresenter @AssistedInject constructor(
|
||||||
roomIds
|
roomIds
|
||||||
.map { roomId ->
|
.map { roomId ->
|
||||||
val room = matrixClient.getRoom(roomId) ?: return@map false
|
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
|
filesToShare
|
||||||
.map { fileToShare ->
|
.map { fileToShare ->
|
||||||
mediaSender.sendMedia(
|
mediaSender.sendMedia(
|
||||||
uri = fileToShare.uri,
|
uri = fileToShare.uri,
|
||||||
mimeType = fileToShare.mimeType,
|
mimeType = fileToShare.mimeType,
|
||||||
compressIfPossible = true,
|
|
||||||
).isSuccess
|
).isSuccess
|
||||||
}
|
}
|
||||||
.all { it }
|
.all { it }
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
|
||||||
posthog = "com.posthog:posthog-android:3.8.2"
|
posthog = "com.posthog:posthog-android:3.8.2"
|
||||||
sentry = "io.sentry:sentry-android:7.15.0"
|
sentry = "io.sentry:sentry-android:7.15.0"
|
||||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
# 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
|
# Emojibase
|
||||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.3.3"
|
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.3.3"
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,12 @@ dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
api(projects.libraries.matrix.api)
|
api(projects.libraries.matrix.api)
|
||||||
|
api(projects.libraries.preferences.api)
|
||||||
implementation(libs.inject)
|
implementation(libs.inject)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(libs.test.junit)
|
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.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
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.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MediaSender @Inject constructor(
|
class MediaSender @Inject constructor(
|
||||||
private val preProcessor: MediaPreProcessor,
|
private val preProcessor: MediaPreProcessor,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) {
|
) {
|
||||||
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
||||||
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
||||||
|
|
@ -27,11 +30,11 @@ class MediaSender @Inject constructor(
|
||||||
suspend fun sendMedia(
|
suspend fun sendMedia(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
compressIfPossible: Boolean,
|
|
||||||
caption: String? = null,
|
caption: String? = null,
|
||||||
formattedCaption: String? = null,
|
formattedCaption: String? = null,
|
||||||
progressCallback: ProgressCallback? = null
|
progressCallback: ProgressCallback? = null
|
||||||
): Result<Unit> {
|
): Result<Unit> {
|
||||||
|
val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
|
||||||
return preProcessor
|
return preProcessor
|
||||||
.process(
|
.process(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
|
@ -49,6 +52,7 @@ class MediaSender @Inject constructor(
|
||||||
}
|
}
|
||||||
.handleSendResult()
|
.handleSendResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendVoiceMessage(
|
suspend fun sendVoiceMessage(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
|
|
@ -60,7 +64,7 @@ class MediaSender @Inject constructor(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
deleteOriginal = true,
|
deleteOriginal = true,
|
||||||
compressIfPossible = false
|
compressIfPossible = false,
|
||||||
)
|
)
|
||||||
.flatMapCatching { info ->
|
.flatMapCatching { info ->
|
||||||
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
|
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.media.FakeMediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
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 io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -33,7 +35,7 @@ class MediaSenderTest {
|
||||||
val sender = aMediaSender(preProcessor)
|
val sender = aMediaSender(preProcessor)
|
||||||
|
|
||||||
val uri = Uri.parse("content://image.jpg")
|
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)
|
assertThat(preProcessor.processCallCount).isEqualTo(1)
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +51,7 @@ class MediaSenderTest {
|
||||||
val sender = aMediaSender(room = room)
|
val sender = aMediaSender(room = room)
|
||||||
|
|
||||||
val uri = Uri.parse("content://image.jpg")
|
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()
|
sendMediaResult.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +63,7 @@ class MediaSenderTest {
|
||||||
val sender = aMediaSender(preProcessor)
|
val sender = aMediaSender(preProcessor)
|
||||||
|
|
||||||
val uri = Uri.parse("content://image.jpg")
|
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()
|
assertThat(result.exceptionOrNull()).isNotNull()
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +76,7 @@ class MediaSenderTest {
|
||||||
val sender = aMediaSender(room = room)
|
val sender = aMediaSender(room = room)
|
||||||
|
|
||||||
val uri = Uri.parse("content://image.jpg")
|
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()
|
assertThat(result.exceptionOrNull()).isNotNull()
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +90,7 @@ class MediaSenderTest {
|
||||||
val sender = aMediaSender(room = room)
|
val sender = aMediaSender(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, compressIfPossible = true)
|
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||||
}
|
}
|
||||||
// Wait until several internal tasks run and the file is being uploaded
|
// Wait until several internal tasks run and the file is being uploaded
|
||||||
advanceTimeBy(3L)
|
advanceTimeBy(3L)
|
||||||
|
|
@ -109,8 +111,10 @@ class MediaSenderTest {
|
||||||
private fun aMediaSender(
|
private fun aMediaSender(
|
||||||
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
room: MatrixRoom = FakeMatrixRoom(),
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
) = MediaSender(
|
) = MediaSender(
|
||||||
preProcessor,
|
preProcessor = preProcessor,
|
||||||
room,
|
room = room,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class ImageCompressor @Inject constructor(
|
||||||
resizeMode: ResizeMode,
|
resizeMode: ResizeMode,
|
||||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
||||||
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||||
desiredQuality: Int = 80,
|
desiredQuality: Int = 78,
|
||||||
): Result<ImageCompressionResult> = withContext(dispatchers.io) {
|
): Result<ImageCompressionResult> = withContext(dispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow()
|
val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow()
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class VideoCompressor @Inject constructor(
|
||||||
val future = Transcoder.into(tmpFile.path)
|
val future = Transcoder.into(tmpFile.path)
|
||||||
.setVideoTrackStrategy(
|
.setVideoTrackStrategy(
|
||||||
DefaultVideoStrategy.Builder()
|
DefaultVideoStrategy.Builder()
|
||||||
.addResizer(AtMostResizer(1920, 1080))
|
.addResizer(AtMostResizer(720, 480))
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.addDataSource(context, uri)
|
.addDataSource(context, uri)
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,9 @@ interface SessionPreferencesStore {
|
||||||
suspend fun setSkipSessionVerification(skip: Boolean)
|
suspend fun setSkipSessionVerification(skip: Boolean)
|
||||||
fun isSessionVerificationSkipped(): Flow<Boolean>
|
fun isSessionVerificationSkipped(): Flow<Boolean>
|
||||||
|
|
||||||
|
suspend fun setCompressMedia(compress: Boolean)
|
||||||
|
fun doesCompressMedia(): Flow<Boolean>
|
||||||
|
|
||||||
|
|
||||||
suspend fun clear()
|
suspend fun clear()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class DefaultSessionPreferencesStore(
|
||||||
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
|
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
|
||||||
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
|
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
|
||||||
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
|
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
|
||||||
|
private val compressMedia = booleanPreferencesKey("compressMedia")
|
||||||
|
|
||||||
private val dataStoreFile = storeFile(context, sessionId)
|
private val dataStoreFile = storeFile(context, sessionId)
|
||||||
private val store = PreferenceDataStoreFactory.create(
|
private val store = PreferenceDataStoreFactory.create(
|
||||||
|
|
@ -81,6 +82,9 @@ class DefaultSessionPreferencesStore(
|
||||||
override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
|
override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
|
||||||
override fun isSessionVerificationSkipped(): Flow<Boolean> = get(skipSessionVerification) { false }
|
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() {
|
override suspend fun clear() {
|
||||||
dataStoreFile.safeDelete()
|
dataStoreFile.safeDelete()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class InMemorySessionPreferencesStore(
|
||||||
isSendTypingNotificationsEnabled: Boolean = true,
|
isSendTypingNotificationsEnabled: Boolean = true,
|
||||||
isRenderTypingNotificationsEnabled: Boolean = true,
|
isRenderTypingNotificationsEnabled: Boolean = true,
|
||||||
isSessionVerificationSkipped: Boolean = false,
|
isSessionVerificationSkipped: Boolean = false,
|
||||||
|
doesCompressMedia: Boolean = false,
|
||||||
) : SessionPreferencesStore {
|
) : SessionPreferencesStore {
|
||||||
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
|
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
|
||||||
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
||||||
|
|
@ -25,6 +26,7 @@ class InMemorySessionPreferencesStore(
|
||||||
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
|
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
|
||||||
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
|
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
|
||||||
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
|
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
|
||||||
|
private val doesCompressMedia = MutableStateFlow(doesCompressMedia)
|
||||||
var clearCallCount = 0
|
var clearCallCount = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
@ -66,6 +68,10 @@ class InMemorySessionPreferencesStore(
|
||||||
return isSessionVerificationSkipped
|
return isSessionVerificationSkipped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun setCompressMedia(compress: Boolean) = doesCompressMedia.emit(compress)
|
||||||
|
|
||||||
|
override fun doesCompressMedia(): Flow<Boolean> = doesCompressMedia
|
||||||
|
|
||||||
override suspend fun clear() {
|
override suspend fun clear() {
|
||||||
clearCallCount++
|
clearCallCount++
|
||||||
isSendPublicReadReceiptsEnabled.tryEmit(true)
|
isSendPublicReadReceiptsEnabled.tryEmit(true)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue