Add media file limit size warning and media quality selection (#5131)

* Add `VideoCompressorPreset` enum

This represents the different compression presets used for processing videos before uploading them

* Add `VideoCompressorHelper` util class to calculate the scaled output size of the video given an input size and its optimal bitrate

Also add `MediaOptimizationConfig` which will be used to decide how to apply compression in `MediaPreProcessor`

* Add `RustMatrixClient.getMaxFileUploadSize()` function and `MaxUploadSizeProvider` so we can import only this functionality into other components

* Try preloading the max file upload size the first time we get network connectivity - it's a best effort

This should help ensure we'll have this value available later, even if we still need to load it asynchronously.

* Split the `compressMedia` preference into `compressImages` and `compressMediaPreset`

* Modify the media processing parts to use the new classes and utils

* Add `MediaOptimizationSelectorPresenter`, which will retrieve the compression values and the max file upload size, also estimating the compressed video file sizes if needed.

* Add a feature flag to allow selecting the media upload quality per upload

* Integrate the previous changes with the attachments preview screen

Add strings from localazy too.

* Adapt the rest of the app calls to upload media to using the media optimization configs

* Allow modifying the default compression values in advanced settings, based on the feature flag value

* Pass the `fileSize` in `MediaUploadInfo` too, to be able to check it against the `maxUploadSize`

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2025-08-11 17:22:46 +02:00 committed by GitHub
parent ffe183c952
commit a170d80cb3
174 changed files with 2152 additions and 340 deletions

View file

@ -0,0 +1,46 @@
/*
* 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.androidutils.media
import android.util.Size
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Helper class to calculate the resulting output size and optimal bitrate for video compression.
*/
class VideoCompressorHelper(
/**
* The maximum size (in pixels) for the output video.
* The output will maintain the aspect ratio of the input video.
*/
val maxSize: Int,
) {
/**
* Calculates the output size for video compression based on the input size and [maxSize].
*/
fun getOutputSize(inputSize: Size): Size {
val resultMajor = min(inputSize.major(), maxSize)
val aspectRatio = inputSize.major().toFloat() / inputSize.minor().toFloat()
return Size(resultMajor, (resultMajor / aspectRatio).roundToInt())
}
/**
* Calculates the optimal bitrate for video compression based on the input size and frame rate.
*/
fun calculateOptimalBitrate(inputSize: Size, frameRate: Int): Long {
val outputSize = getOutputSize(inputSize)
val pixelsPerFrame = outputSize.width * outputSize.height
// Apparently, 0.1 bits per pixel is a sweet spot for video compression
val bitsPerPixel = 0.1f
return (pixelsPerFrame * bitsPerPixel * frameRate).toLong() / 1000
}
}
internal fun Size.major(): Int = if (width > height) width else height
internal fun Size.minor(): Int = if (width < height) width else height

View file

@ -184,4 +184,12 @@ enum class FeatureFlags(
// False so it's displayed in the developer options screen
isFinished = false,
),
SelectableMediaQuality(
key = "feature.selectable_media_quality",
title = "Select media quality per upload",
description = "You can select the media quality for each attachment you upload.",
defaultValue = { false },
// False so it's displayed in the developer options screen
isFinished = false,
),
}

View file

@ -171,6 +171,11 @@ interface MatrixClient {
* Return true if Livekit Rtc is supported, i.e. if Element Call is available.
*/
suspend fun isLivekitRtcSupported(): Boolean
/**
* Returns the maximum file upload size allowed by the Matrix server.
*/
suspend fun getMaxFileUploadSize(): Result<Long>
}
/**

View file

@ -692,6 +692,10 @@ class RustMatrixClient(
innerClient.isLivekitRtcSupported()
}
override suspend fun getMaxFileUploadSize(): Result<Long> = withContext(sessionDispatcher) {
runCatchingExceptions { innerClient.getMaxMediaUploadSize().toLong() }
}
private suspend fun File.getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {

View file

@ -94,6 +94,7 @@ class FakeMatrixClient(
private val canReportRoomLambda: () -> Boolean = { false },
private val isLivekitRtcSupportedLambda: () -> Boolean = { false },
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
private val getMaxUploadSizeResult: () -> Result<Long> = { lambdaError() },
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
private set
@ -343,4 +344,8 @@ class FakeMatrixClient(
override suspend fun isLivekitRtcSupported(): Boolean {
return isLivekitRtcSupportedLambda()
}
override suspend fun getMaxFileUploadSize(): Result<Long> {
return getMaxUploadSizeResult()
}
}

View file

@ -0,0 +1,22 @@
/*
* 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.api
import io.element.android.libraries.matrix.api.MatrixClient
import javax.inject.Inject
/**
* Provides the maximum upload size allowed by the Matrix server.
*/
class MaxUploadSizeProvider @Inject constructor(
private val matrixClient: MatrixClient,
) {
suspend fun getMaxUploadSize(): Result<Long> {
return matrixClient.getMaxFileUploadSize()
}
}

View file

@ -0,0 +1,22 @@
/*
* 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.api
import io.element.android.libraries.androidutils.media.VideoCompressorHelper
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
data class MediaOptimizationConfig(
val compressImages: Boolean,
val videoCompressionPreset: VideoCompressionPreset,
)
fun VideoCompressionPreset.compressorHelper(): VideoCompressorHelper = when (this) {
VideoCompressionPreset.STANDARD -> VideoCompressorHelper(1280)
VideoCompressionPreset.HIGH -> VideoCompressorHelper(1920)
VideoCompressionPreset.LOW -> VideoCompressorHelper(640)
}

View file

@ -0,0 +1,12 @@
/*
* 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.api
fun interface MediaOptimizationConfigProvider {
suspend fun get(): MediaOptimizationConfig
}

View file

@ -19,7 +19,7 @@ interface MediaPreProcessor {
uri: Uri,
mimeType: String,
deleteOriginal: Boolean,
compressIfPossible: Boolean,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo>
/**

View file

@ -14,17 +14,15 @@ 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.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
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: JoinedRoom,
private val sessionPreferencesStore: SessionPreferencesStore,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
) {
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
@ -32,14 +30,14 @@ class MediaSender @Inject constructor(
suspend fun preProcessMedia(
uri: Uri,
mimeType: String,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo> {
val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
return preProcessor
.process(
uri = uri,
mimeType = mimeType,
deleteOriginal = false,
compressIfPossible = compressIfPossible,
mediaOptimizationConfig = mediaOptimizationConfig,
)
}
@ -67,14 +65,14 @@ class MediaSender @Inject constructor(
formattedCaption: String? = null,
progressCallback: ProgressCallback? = null,
inReplyToEventId: EventId? = null,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<Unit> {
val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
return preProcessor
.process(
uri = uri,
mimeType = mimeType,
deleteOriginal = false,
compressIfPossible = compressIfPossible,
mediaOptimizationConfig = mediaOptimizationConfig,
)
.flatMapCatching { info ->
room.liveTimeline.sendMedia(
@ -100,7 +98,7 @@ class MediaSender @Inject constructor(
uri = uri,
mimeType = mimeType,
deleteOriginal = true,
compressIfPossible = false,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
)
.flatMapCatching { info ->
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo

View file

@ -19,8 +19,7 @@ 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.timeline.FakeTimeline
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.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@ -34,6 +33,11 @@ import java.io.File
@RunWith(RobolectricTestRunner::class)
class MediaSenderTest {
private val mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
)
@Test
fun `given an attachment when sending it the preprocessor always runs`() = runTest {
val preProcessor = FakeMediaPreProcessor()
@ -57,7 +61,7 @@ class MediaSenderTest {
)
val uri = Uri.parse("content://image.jpg")
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
assertThat(preProcessor.processCallCount).isEqualTo(1)
}
@ -76,7 +80,7 @@ class MediaSenderTest {
val sender = createMediaSender(room = room)
val uri = Uri.parse("content://image.jpg")
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
}
@Test
@ -87,7 +91,7 @@ class MediaSenderTest {
val sender = createMediaSender(preProcessor)
val uri = Uri.parse("content://image.jpg")
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
assertThat(result.exceptionOrNull()).isNotNull()
}
@ -112,7 +116,7 @@ class MediaSenderTest {
)
val uri = Uri.parse("content://image.jpg")
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
assertThat(result.exceptionOrNull()).isNotNull()
}
@ -132,7 +136,7 @@ class MediaSenderTest {
val sender = createMediaSender(room = room)
val sendJob = launch {
val uri = Uri.parse("content://image.jpg")
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
}
// Wait until several internal tasks run and the file is being uploaded
advanceTimeBy(3L)
@ -154,10 +158,10 @@ class MediaSenderTest {
private fun createMediaSender(
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
room: JoinedRoom = FakeJoinedRoom(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = MediaOptimizationConfigProvider { mediaOptimizationConfig },
) = MediaSender(
preProcessor = preProcessor,
room = room,
sessionPreferencesStore = sessionPreferencesStore,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
)
}

View file

@ -32,8 +32,10 @@ import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
@ -76,7 +78,7 @@ class AndroidMediaPreProcessor @Inject constructor(
uri: Uri,
mimeType: String,
deleteOriginal: Boolean,
compressIfPossible: Boolean,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo> = withContext(coroutineDispatchers.computation) {
runCatchingExceptions {
val result = when {
@ -85,10 +87,10 @@ class AndroidMediaPreProcessor @Inject constructor(
processFile(uri, mimeType)
}
mimeType.isMimeTypeImage() -> {
val shouldBeCompressed = compressIfPossible && mimeType !in notCompressibleImageTypes
val shouldBeCompressed = mediaOptimizationConfig.compressImages && mimeType !in notCompressibleImageTypes
processImage(uri, mimeType, shouldBeCompressed)
}
mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType, compressIfPossible)
mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType, mediaOptimizationConfig.videoCompressionPreset)
mimeType.isMimeTypeAudio() -> processAudio(uri, mimeType)
else -> processFile(uri, mimeType)
}
@ -214,9 +216,9 @@ class AndroidMediaPreProcessor @Inject constructor(
}
}
private suspend fun processVideo(uri: Uri, mimeType: String?, shouldBeCompressed: Boolean): MediaUploadInfo {
private suspend fun processVideo(uri: Uri, mimeType: String?, videoCompressionPreset: VideoCompressionPreset): MediaUploadInfo {
val resultFile = runCatchingExceptions {
videoCompressor.compress(uri, shouldBeCompressed)
videoCompressor.compress(uri, videoCompressionPreset)
.onEach {
if (it is VideoTranscodingEvent.Progress) {
Timber.d("Video compression progress: ${it.value}%")

View file

@ -0,0 +1,26 @@
/*
* 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 com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.coroutines.flow.first
import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultMediaOptimizationConfigProvider @Inject constructor(
private val sessionPreferencesStore: SessionPreferencesStore,
) : MediaOptimizationConfigProvider {
override suspend fun get(): MediaOptimizationConfig = MediaOptimizationConfig(
compressImages = sessionPreferencesStore.doesOptimizeImages().first(),
videoCompressionPreset = sessionPreferencesStore.getVideoCompressionPreset().first(),
)
}

View file

@ -11,10 +11,10 @@ import android.content.Context
import android.media.MediaCodecInfo
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.util.Size
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.Size
import androidx.media3.common.util.UnstableApi
import androidx.media3.effect.Presentation
import androidx.media3.transformer.Composition
@ -31,6 +31,7 @@ import io.element.android.libraries.androidutils.file.createTmpFile
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
@ -47,12 +48,12 @@ class VideoCompressor @Inject constructor(
@ApplicationContext private val context: Context,
) {
@OptIn(UnstableApi::class)
fun compress(uri: Uri, shouldBeCompressed: Boolean): Flow<VideoTranscodingEvent> = callbackFlow {
fun compress(uri: Uri, videoCompressionPreset: VideoCompressionPreset): Flow<VideoTranscodingEvent> = callbackFlow {
val metadata = getVideoMetadata(uri)
val videoCompressorConfig = VideoCompressorConfigFactory.create(
metadata = metadata,
shouldBeCompressed = shouldBeCompressed
preset = videoCompressionPreset,
)
val tmpFile = context.createTmpFile(extension = "mp4")
@ -60,7 +61,7 @@ class VideoCompressor @Inject constructor(
val width = metadata?.width ?: Int.MAX_VALUE
val height = metadata?.height ?: Int.MAX_VALUE
val videoResizeEffect = videoCompressorConfig.resizer?.let {
val videoResizeEffect = videoCompressorConfig.videoCompressorHelper?.let {
val outputSize = it.getOutputSize(Size(width, height))
if (metadata?.rotation == 90 || metadata?.rotation == 270) {
// If the video is rotated, we need to swap width and height

View file

@ -7,62 +7,37 @@
package io.element.android.libraries.mediaupload.impl
import android.util.Size
import androidx.annotation.OptIn
import androidx.media3.common.util.Size
import androidx.media3.common.util.UnstableApi
import androidx.media3.transformer.VideoEncoderSettings
import io.element.android.libraries.androidutils.media.VideoCompressorHelper
import io.element.android.libraries.mediaupload.api.compressorHelper
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlin.math.min
import kotlin.math.roundToInt
@OptIn(UnstableApi::class)
internal object VideoCompressorConfigFactory {
// Major dimension of 720p
private const val MAX_COMPRESSED_PIXEL_SIZE = 1280
// Major dimension of 1080p
private const val MAX_PIXEL_SIZE = 1920
private const val DEFAULT_FRAME_RATE = 30
fun create(
metadata: VideoFileMetadata?,
shouldBeCompressed: Boolean,
preset: VideoCompressionPreset,
): VideoCompressorConfig {
val width = metadata?.width?.takeIf { it >= 0 } ?: Int.MAX_VALUE
val height = metadata?.height?.takeIf { it >= 0 } ?: Int.MAX_VALUE
val originalBitrate = metadata?.bitrate?.takeIf { it >= 0 }
val originalFrameRate = metadata?.frameRate?.takeIf { it >= 0 } ?: DEFAULT_FRAME_RATE
// We only create a resizer if needed
val resizer = when {
shouldBeCompressed && (width > MAX_COMPRESSED_PIXEL_SIZE || height > MAX_COMPRESSED_PIXEL_SIZE) -> VideoResizer(MAX_COMPRESSED_PIXEL_SIZE)
width > MAX_PIXEL_SIZE || height > MAX_PIXEL_SIZE -> VideoResizer(MAX_PIXEL_SIZE)
else -> null
}
val resizer = preset.compressorHelper()
// If we are resizing, we also want to reduce the frame rate to the default value (30fps)
val newFrameRate = if (resizer is VideoResizer) {
min(originalFrameRate, DEFAULT_FRAME_RATE)
} else {
originalFrameRate
}
val newFrameRate = min(originalFrameRate, DEFAULT_FRAME_RATE)
// If we need to resize the video, we also want to recalculate the bitrate
val newBitrate = if (resizer is VideoResizer) {
val maxSize = resizer.getOutputSize(Size(width, height))
val pixelsPerFrame = maxSize.width * maxSize.height
val frameRate = newFrameRate
// Apparently, 0.1 bits per pixel is a sweet spot for video compression
val bitsPerPixel = 0.1f
(pixelsPerFrame * bitsPerPixel * frameRate).toLong()
} else {
originalBitrate
}
val newBitrate = resizer.calculateOptimalBitrate(Size(width, height), newFrameRate)
return VideoCompressorConfig(
resizer = resizer,
newBitRate = newBitrate?.toInt() ?: VideoEncoderSettings.NO_VALUE,
videoCompressorHelper = resizer,
newBitRate = newBitrate.toInt(),
newFrameRate = newFrameRate,
)
}
@ -70,28 +45,7 @@ internal object VideoCompressorConfigFactory {
@OptIn(UnstableApi::class)
internal data class VideoCompressorConfig(
val resizer: VideoResizer?,
val videoCompressorHelper: VideoCompressorHelper,
val newBitRate: Int,
val newFrameRate: Int,
)
@OptIn(UnstableApi::class)
internal class VideoResizer(
val maxSize: Int,
) {
fun getOutputSize(inputSize: Size): Size {
val resultMajor = min(inputSize.major(), maxSize)
val aspectRatio = inputSize.width.toFloat() / inputSize.height.toFloat()
return if (inputSize.width > inputSize.height) {
Size(resultMajor, (resultMajor / aspectRatio).roundToInt())
} else {
Size((resultMajor * aspectRatio).roundToInt(), resultMajor)
}
}
}
@OptIn(UnstableApi::class)
internal fun Size.major(): Int = if (width > height) width else height
@OptIn(UnstableApi::class)
internal fun Size.minor(): Int = if (width < height) width else height

View file

@ -20,8 +20,10 @@ import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter
import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -41,7 +43,7 @@ import kotlin.time.Duration
class AndroidMediaPreProcessorTest {
private suspend fun TestScope.process(
asset: Asset,
compressIfPossible: Boolean,
mediaOptimizationConfig: MediaOptimizationConfig,
sdkIntVersion: Int = Build.VERSION_CODES.P,
deleteOriginal: Boolean = false,
): MediaUploadInfo {
@ -57,7 +59,7 @@ class AndroidMediaPreProcessorTest {
uri = file.toUri(),
mimeType = asset.mimeType,
deleteOriginal = deleteOriginal,
compressIfPossible = compressIfPossible,
mediaOptimizationConfig = mediaOptimizationConfig,
)
val data = result.getOrThrow()
assertThat(data.file.path).endsWith(asset.filename)
@ -70,7 +72,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing png`() = runTest {
val mediaUploadInfo = process(
asset = assetImagePng,
compressIfPossible = true,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNotNull()
@ -91,7 +96,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing png api Q`() = runTest {
val mediaUploadInfo = process(
asset = assetImagePng,
compressIfPossible = true,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
sdkIntVersion = Build.VERSION_CODES.Q,
)
val info = mediaUploadInfo as MediaUploadInfo.Image
@ -114,7 +122,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing png no compression`() = runTest {
val mediaUploadInfo = process(
asset = assetImagePng,
compressIfPossible = false,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = false,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNotNull()
@ -136,7 +147,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing png and delete`() = runTest {
val mediaUploadInfo = process(
asset = assetImagePng,
compressIfPossible = false,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = false,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
deleteOriginal = true,
)
val info = mediaUploadInfo as MediaUploadInfo.Image
@ -161,7 +175,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing jpeg`() = runTest {
val mediaUploadInfo = process(
asset = assetImageJpeg,
compressIfPossible = true,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNotNull()
@ -182,7 +199,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing jpeg api Q`() = runTest {
val mediaUploadInfo = process(
asset = assetImageJpeg,
compressIfPossible = true,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
sdkIntVersion = Build.VERSION_CODES.Q,
)
val info = mediaUploadInfo as MediaUploadInfo.Image
@ -205,7 +225,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing jpeg no compression`() = runTest {
val mediaUploadInfo = process(
asset = assetImageJpeg,
compressIfPossible = false,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = false,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNotNull()
@ -227,7 +250,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing jpeg and delete`() = runTest {
val mediaUploadInfo = process(
asset = assetImageJpeg,
compressIfPossible = false,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = false,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
deleteOriginal = true,
)
val info = mediaUploadInfo as MediaUploadInfo.Image
@ -252,7 +278,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing gif`() = runTest {
val mediaUploadInfo = process(
asset = assetAnimatedGif,
compressIfPossible = true,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNotNull()
@ -273,7 +302,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing file`() = runTest {
val mediaUploadInfo = process(
asset = assetText,
compressIfPossible = true,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.AnyFile
assertThat(info.fileInfo).isEqualTo(
@ -291,7 +323,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing video`() = runTest {
val mediaUploadInfo = process(
asset = assetVideo,
compressIfPossible = true,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Video
assertThat(info.thumbnailFile).isNotNull()
@ -315,7 +350,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing video no compression`() = runTest {
val mediaUploadInfo = process(
asset = assetVideo,
compressIfPossible = false,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.HIGH,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Video
// Computing thumbnailFile is failing with Robolectric
@ -341,7 +379,10 @@ class AndroidMediaPreProcessorTest {
fun `test processing audio`() = runTest {
val mediaUploadInfo = process(
asset = assetAudio,
compressIfPossible = true,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Audio
assertThat(info.audioInfo).isEqualTo(
@ -363,7 +404,10 @@ class AndroidMediaPreProcessorTest {
uri = file.toUri(),
mimeType = MimeTypes.PlainText,
deleteOriginal = false,
compressIfPossible = true,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
assertThat(result.isFailure).isTrue()
val failure = result.exceptionOrNull()

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.mediaupload.impl
import androidx.media3.transformer.VideoEncoderSettings
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@ -20,16 +21,16 @@ class VideoCompressorConfigFactoryTest {
fun `if we don't have metadata the video will be resized`() {
// Given
val metadata = null
val shouldBeCompressed = false
val preset = VideoCompressionPreset.STANDARD
// When
val videoCompressorConfig = VideoCompressorConfigFactory.create(
metadata = metadata,
shouldBeCompressed = shouldBeCompressed
preset = preset,
)
// Then
assertThat(videoCompressorConfig.resizer).isNotNull()
assertThat(videoCompressorConfig.videoCompressorHelper).isNotNull()
assertThat(videoCompressorConfig.newFrameRate).isEqualTo(30)
assertThat(videoCompressorConfig.newBitRate).isNotEqualTo(VideoEncoderSettings.NO_VALUE)
}
@ -38,71 +39,71 @@ class VideoCompressorConfigFactoryTest {
fun `if the video should be compressed and is larger than 720p it will be resized`() {
// Given
val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = true
val preset = VideoCompressionPreset.STANDARD
// When
val videoCompressorConfig = VideoCompressorConfigFactory.create(
metadata = metadata,
shouldBeCompressed = shouldBeCompressed
preset = preset,
)
// Then
assertIsResized(videoCompressorConfig)
assertIsResized(videoCompressorConfig, metadata.width)
}
@Test
fun `if the video should be compressed and is smaller or equal to 720p it will not be resized`() {
// Given
val metadata = VideoFileMetadata(width = 1280, height = 720, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = true
val preset = VideoCompressionPreset.STANDARD
// When
val videoCompressorConfig = VideoCompressorConfigFactory.create(
metadata = metadata,
shouldBeCompressed = shouldBeCompressed
preset = preset,
)
// Then
assertIsNotResized(videoCompressorConfig)
assertIsNotResized(videoCompressorConfig, 1280)
}
@Test
fun `if the video should not be compressed and is larger than 1080p it will be resized`() {
// Given
val metadata = VideoFileMetadata(width = 2560, height = 1440, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = false
val preset = VideoCompressionPreset.HIGH
// When
val videoCompressorConfig = VideoCompressorConfigFactory.create(
metadata = metadata,
shouldBeCompressed = shouldBeCompressed
preset = preset,
)
// Then
assertIsResized(videoCompressorConfig)
assertIsResized(videoCompressorConfig, metadata.width)
}
@Test
fun `if the video should not be compressed and is smaller or equal than 1080p it will not be resized`() {
// Given
val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = false
val preset = VideoCompressionPreset.HIGH
// When
val videoCompressorConfig = VideoCompressorConfigFactory.create(
metadata = metadata,
shouldBeCompressed = shouldBeCompressed
preset = preset,
)
// Then
assertIsNotResized(videoCompressorConfig)
assertIsNotResized(videoCompressorConfig, 1920)
}
private inline fun assertIsResized(videoCompressorConfig: VideoCompressorConfig) {
assertThat(videoCompressorConfig.resizer).isNotNull()
private inline fun assertIsResized(videoCompressorConfig: VideoCompressorConfig, referenceSize: Int) {
assertThat(videoCompressorConfig.videoCompressorHelper.maxSize).isNotEqualTo(referenceSize)
}
private inline fun assertIsNotResized(videoCompressorConfig: VideoCompressorConfig) {
assertThat(videoCompressorConfig.resizer).isNull()
private inline fun assertIsNotResized(videoCompressorConfig: VideoCompressorConfig, referenceSize: Int) {
assertThat(videoCompressorConfig.videoCompressorHelper.maxSize).isEqualTo(referenceSize)
}
}

View file

@ -0,0 +1,21 @@
/*
* 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.test
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
class FakeMediaOptimizationConfigProvider(
val config: MediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
)
) : MediaOptimizationConfigProvider {
override suspend fun get(): MediaOptimizationConfig = config
}

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.tests.testutils.simulateLongTask
@ -45,7 +46,7 @@ class FakeMediaPreProcessor(
uri: Uri,
mimeType: String,
deleteOriginal: Boolean,
compressIfPossible: Boolean
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo> = simulateLongTask {
processLatch?.await()
processCallCount++

View file

@ -17,6 +17,7 @@ data class MediaInfo(
val filename: String,
val caption: String?,
val mimeType: String,
val fileSize: Long?,
val formattedFileSize: String,
val fileExtension: String,
val senderId: UserId?,
@ -36,6 +37,7 @@ fun anImageMediaInfo(
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = "an image file.jpg",
fileSize = 4 * 1024 * 1024,
caption = caption,
mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB",
@ -57,6 +59,7 @@ fun aVideoMediaInfo(
duration: String? = null,
): MediaInfo = MediaInfo(
filename = "a video file.mp4",
fileSize = 14 * 1024 * 1024,
caption = caption,
mimeType = MimeTypes.Mp4,
formattedFileSize = "14MB",
@ -78,6 +81,7 @@ fun aPdfMediaInfo(
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = filename,
fileSize = 23 * 1024 * 1024,
caption = caption,
mimeType = MimeTypes.Pdf,
formattedFileSize = "23MB",
@ -98,6 +102,7 @@ fun anApkMediaInfo(
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = "an apk file.apk",
fileSize = 50 * 1024 * 1024,
caption = null,
mimeType = MimeTypes.Apk,
formattedFileSize = "50MB",
@ -121,6 +126,7 @@ fun anAudioMediaInfo(
duration: String? = null,
): MediaInfo = MediaInfo(
filename = filename,
fileSize = 7 * 1024 * 1024,
caption = caption,
mimeType = MimeTypes.Mp3,
formattedFileSize = "7MB",
@ -144,6 +150,7 @@ fun aVoiceMediaInfo(
duration: String? = null,
): MediaInfo = MediaInfo(
filename = filename,
fileSize = 3 * 1024 * 1024,
caption = caption,
mimeType = MimeTypes.Ogg,
formattedFileSize = "3MB",
@ -165,6 +172,7 @@ fun aTxtMediaInfo(
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = filename,
fileSize = 2 * 1024,
caption = caption,
mimeType = MimeTypes.PlainText,
formattedFileSize = "2kB",

View file

@ -46,6 +46,7 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
eventId = null,
mediaInfo = MediaInfo(
filename = filename,
fileSize = null,
caption = null,
mimeType = mimeType,
formattedFileSize = "",

View file

@ -92,6 +92,7 @@ class EventItemFactory @Inject constructor(
eventId = currentTimelineItem.eventId,
mediaInfo = MediaInfo(
filename = type.filename,
fileSize = type.info?.size,
caption = type.caption,
mimeType = type.info?.mimetype.orEmpty(),
formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
@ -111,6 +112,7 @@ class EventItemFactory @Inject constructor(
eventId = currentTimelineItem.eventId,
mediaInfo = MediaInfo(
filename = type.filename,
fileSize = type.info?.size,
caption = type.caption,
mimeType = type.info?.mimetype.orEmpty(),
formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
@ -131,6 +133,7 @@ class EventItemFactory @Inject constructor(
eventId = currentTimelineItem.eventId,
mediaInfo = MediaInfo(
filename = type.filename,
fileSize = type.info?.size,
caption = type.caption,
mimeType = type.info?.mimetype.orEmpty(),
formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
@ -151,6 +154,7 @@ class EventItemFactory @Inject constructor(
eventId = currentTimelineItem.eventId,
mediaInfo = MediaInfo(
filename = type.filename,
fileSize = type.info?.size,
caption = type.caption,
mimeType = type.info?.mimetype.orEmpty(),
formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
@ -171,6 +175,7 @@ class EventItemFactory @Inject constructor(
eventId = currentTimelineItem.eventId,
mediaInfo = MediaInfo(
filename = type.filename,
fileSize = type.info?.size,
caption = type.caption,
mimeType = type.info?.mimetype.orEmpty(),
formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
@ -191,6 +196,7 @@ class EventItemFactory @Inject constructor(
eventId = currentTimelineItem.eventId,
mediaInfo = MediaInfo(
filename = type.filename,
fileSize = type.info?.size,
caption = type.caption,
mimeType = type.info?.mimetype.orEmpty(),
formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),

View file

@ -87,15 +87,17 @@ class AndroidLocalMediaFactory @Inject constructor(
): LocalMedia {
val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream
val fileName = name ?: context.getFileName(uri) ?: ""
val fileSize = formattedFileSize ?: fileSizeFormatter.format(context.getFileSize(uri))
val fileSize = context.getFileSize(uri)
val calculatedFormattedFileSize = formattedFileSize ?: fileSizeFormatter.format(fileSize)
val fileExtension = fileExtensionExtractor.extractFromName(fileName)
return LocalMedia(
uri = uri,
info = MediaInfo(
mimeType = resolvedMimeType,
filename = fileName,
fileSize = fileSize,
caption = caption,
formattedFileSize = fileSize,
formattedFileSize = calculatedFormattedFileSize,
fileExtension = fileExtension,
senderId = senderId,
senderName = senderName,

View file

@ -155,6 +155,7 @@ class DefaultEventItemFactoryTest {
mediaInfo = MediaInfo(
mimeType = MimeTypes.Apk,
filename = "filename.apk",
fileSize = 123L,
caption = "caption",
formattedFileSize = "123 Bytes",
fileExtension = "apk",
@ -205,6 +206,7 @@ class DefaultEventItemFactoryTest {
mediaInfo = MediaInfo(
mimeType = MimeTypes.Jpeg,
filename = "filename.jpg",
fileSize = 123L,
caption = "caption",
formattedFileSize = "123 Bytes",
fileExtension = "jpg",
@ -252,6 +254,7 @@ class DefaultEventItemFactoryTest {
mediaInfo = MediaInfo(
mimeType = MimeTypes.Mp3,
filename = "filename.mp3",
fileSize = 123L,
caption = "caption",
formattedFileSize = "123 Bytes",
fileExtension = "mp3",
@ -303,6 +306,7 @@ class DefaultEventItemFactoryTest {
mediaInfo = MediaInfo(
mimeType = MimeTypes.Mp4,
filename = "filename.mp4",
fileSize = 123L,
caption = "caption",
formattedFileSize = "123 Bytes",
fileExtension = "mp4",
@ -354,6 +358,7 @@ class DefaultEventItemFactoryTest {
mediaInfo = MediaInfo(
mimeType = MimeTypes.Ogg,
filename = "filename.ogg",
fileSize = 123L,
caption = "caption",
formattedFileSize = "123 Bytes",
fileExtension = "ogg",
@ -404,6 +409,7 @@ class DefaultEventItemFactoryTest {
mediaInfo = MediaInfo(
mimeType = MimeTypes.Gif,
filename = "filename.gif",
fileSize = 123L,
caption = "caption",
formattedFileSize = "123 Bytes",
fileExtension = "gif",

View file

@ -232,6 +232,7 @@ class TimelineMediaGalleryDataSourceTest {
eventId = AN_EVENT_ID,
mediaInfo = MediaInfo(
filename = "body.jpg",
fileSize = 888L,
caption = "body.jpg caption",
mimeType = MimeTypes.Jpeg,
formattedFileSize = "888 Bytes",

View file

@ -40,6 +40,8 @@ class AndroidLocalMediaFactoryTest {
assertThat(result.info).isEqualTo(
MediaInfo(
filename = "an image file.jpg",
// MediaFile does not provide file size in this test
fileSize = 0L,
caption = null,
mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB",

View file

@ -29,10 +29,16 @@ class FakeLocalMediaFactory(
return aLocalMedia(uri = localMediaUri, mediaInfo = mediaInfo)
}
override fun createFromUri(uri: Uri, mimeType: String?, name: String?, formattedFileSize: String?): LocalMedia {
override fun createFromUri(
uri: Uri,
mimeType: String?,
name: String?,
formattedFileSize: String?
): LocalMedia {
val safeName = name ?: fallbackName
val mediaInfo = MediaInfo(
filename = safeName,
fileSize = null,
caption = null,
mimeType = mimeType ?: fallbackMimeType,
formattedFileSize = formattedFileSize ?: fallbackFileSize,

View file

@ -28,8 +28,11 @@ interface SessionPreferencesStore {
suspend fun setSkipSessionVerification(skip: Boolean)
fun isSessionVerificationSkipped(): Flow<Boolean>
suspend fun setCompressMedia(compress: Boolean)
fun doesCompressMedia(): Flow<Boolean>
suspend fun setOptimizeImages(compress: Boolean)
fun doesOptimizeImages(): Flow<Boolean>
suspend fun setVideoCompressionPreset(preset: VideoCompressionPreset)
fun getVideoCompressionPreset(): Flow<VideoCompressionPreset>
suspend fun clear()
}

View file

@ -0,0 +1,22 @@
/*
* 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.preferences.api.store
/**
* Video compression presets to use when processing videos before uploading them.
*/
enum class VideoCompressionPreset {
/** High quality compression, suitable for high-resolution videos. */
HIGH,
/** Standard quality compression, suitable for most videos. */
STANDARD,
/** Low quality compression, suitable for low-resolution videos or when bandwidth is a concern. */
LOW
}

View file

@ -12,12 +12,15 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@ -41,7 +44,8 @@ class DefaultSessionPreferencesStore(
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
private val compressMedia = booleanPreferencesKey("compressMedia")
private val compressImages = booleanPreferencesKey("compressMedia")
private val compressMediaPreset = stringPreferencesKey("compressMediaPreset")
private val dataStoreFile = storeFile(context, sessionId)
private val store = PreferenceDataStoreFactory.create(
@ -82,8 +86,12 @@ 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) { true }
override suspend fun setOptimizeImages(compress: Boolean) = update(compressImages, compress)
override fun doesOptimizeImages(): Flow<Boolean> = get(compressImages) { true }
override suspend fun setVideoCompressionPreset(preset: VideoCompressionPreset) = update(compressMediaPreset, preset.name)
override fun getVideoCompressionPreset(): Flow<VideoCompressionPreset> = get(compressMediaPreset) { VideoCompressionPreset.STANDARD.name }
.map { tryOrNull { VideoCompressionPreset.valueOf(it) } ?: VideoCompressionPreset.STANDARD }
override suspend fun clear() {
dataStoreFile.safeDelete()

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.preferences.test
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -19,6 +20,7 @@ class InMemorySessionPreferencesStore(
isRenderTypingNotificationsEnabled: Boolean = true,
isSessionVerificationSkipped: Boolean = false,
doesCompressMedia: Boolean = true,
videoCompressionPreset: VideoCompressionPreset = VideoCompressionPreset.STANDARD,
) : SessionPreferencesStore {
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
@ -27,6 +29,7 @@ class InMemorySessionPreferencesStore(
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
private val doesCompressMedia = MutableStateFlow(doesCompressMedia)
private val videoCompressionPreset = MutableStateFlow(videoCompressionPreset)
var clearCallCount = 0
private set
@ -68,9 +71,17 @@ class InMemorySessionPreferencesStore(
return isSessionVerificationSkipped
}
override suspend fun setCompressMedia(compress: Boolean) = doesCompressMedia.emit(compress)
override suspend fun setOptimizeImages(compress: Boolean) = doesCompressMedia.emit(compress)
override fun doesCompressMedia(): Flow<Boolean> = doesCompressMedia
override fun doesOptimizeImages(): Flow<Boolean> = doesCompressMedia
override suspend fun setVideoCompressionPreset(preset: VideoCompressionPreset) {
videoCompressionPreset.value = preset
}
override fun getVideoCompressionPreset(): Flow<VideoCompressionPreset> {
return videoCompressionPreset
}
override suspend fun clear() {
clearCallCount++

View file

@ -48,7 +48,7 @@ fun CaptionWarningBottomSheet(
style = BigIcon.Style.AlertSolid,
)
Text(
text = stringResource(CommonStrings.screen_media_upload_preview_caption_warning),
text = stringResource(R.string.screen_media_upload_preview_caption_warning),
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,

View file

@ -28,5 +28,6 @@
<string name="rich_text_editor_remove_link">"Remove link"</string>
<string name="rich_text_editor_unindent">"Unindent"</string>
<string name="rich_text_editor_url_placeholder">"Link"</string>
<string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string>
<string name="screen_room_voice_message_tooltip">"Hold to record"</string>
</resources>

View file

@ -297,8 +297,6 @@
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Паведаміць аб памылцы з дапамогай Rageshake"</string>
<string name="screen_media_picker_error_failed_selection">"Не ўдалося выбраць носьбіт, паўтарыце спробу."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."</string>
<string name="screen_pinned_timeline_empty_state_description">"Націсніце на паведамленне і абярыце «%1$s », каб уключыць сюды."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Замацуеце важныя паведамленні, каб іх можна было лёгка знайсці"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -383,12 +383,6 @@ Opravdu chcete pokračovat?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Odstranit %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Nastavení"</string>
<string name="screen_media_picker_error_failed_selection">"Výběr média se nezdařil, zkuste to prosím znovu."</string>
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Soubor nelze nahrát."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Maximální povolená velikost souboru je %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Soubor je pro nahrání příliš velký."</string>
<string name="screen_pinned_timeline_empty_state_description">"Přidržte zprávu a vyberte „%1$s“, kterou chcete zahrnout sem."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Připněte důležité zprávy, aby je bylo možné snadno najít"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -391,9 +391,6 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Tynnu %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Gosodiadau"</string>
<string name="screen_media_picker_error_failed_selection">"Wedi methu dewis cyfrwng, ceisiwch eto."</string>
<string name="screen_media_upload_preview_caption_warning">"Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Wedi methu llwytho cyfryngau, ceisiwch eto."</string>
<string name="screen_pinned_timeline_empty_state_description">"Pwyswch ar neges a dewis “%1$s” i\'w cynnwys yma."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Pinio negeseuon pwysig fel y mae modd eu darganfod yn hawdd"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -379,12 +379,6 @@ Er du sikker på, at du vil fortsætte?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Fjern %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Indstillinger"</string>
<string name="screen_media_picker_error_failed_selection">"Det lykkedes ikke at vælge medie. Prøv igen."</string>
<string name="screen_media_upload_preview_caption_warning">"Billedtekster er muligvis ikke synlige for personer, der bruger ældre apps."</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunne ikke uploades."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Det lykkedes ikke at behandle medier til upload. Prøv venligst igen."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Upload af medier mislykkedes. Prøv igen."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Den maksimalt tilladte filstørrelse er %1$s ."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Filen er for stor til at kunne uploades."</string>
<string name="screen_pinned_timeline_empty_state_description">"Tryk på en besked og vælg \"%1$s\" for at inkludere den her."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Fastgør vigtige beskeder, så de let kan opdages"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -375,9 +375,6 @@ Möchten Sie wirklich fortfahren?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Entferne %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Einstellungen"</string>
<string name="screen_media_picker_error_failed_selection">"Medienauswahl fehlgeschlagen, bitte versuche es erneut."</string>
<string name="screen_media_upload_preview_caption_warning">"Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut."</string>
<string name="screen_pinned_timeline_empty_state_description">"Drücke auf eine Nachricht und wähle “%1$s”, um sie hier einzufügen."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Fixiere wichtige Nachrichten, so dass sie leicht gefunden werden können"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -360,9 +360,6 @@
<string name="screen_create_poll_remove_accessibility_label">"Αφαίρεση %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Ρυθμίσεις"</string>
<string name="screen_media_picker_error_failed_selection">"Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά."</string>
<string name="screen_media_upload_preview_caption_warning">"Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Αποτυχία μεταφόρτωσης πολυμέσων, δοκίμασε ξανά."</string>
<string name="screen_pinned_timeline_empty_state_description">"Πάτα σε ένα μήνυμα και επέλεξε «%1$s» για να συμπεριληφθεί εδώ."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Καρφίτσωσε σημαντικά μηνύματα, ώστε να μπορούν να εντοπιστούν εύκολα"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -346,9 +346,6 @@ Motivo: %1$s."</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Agitar con fuerza para informar de un error"</string>
<string name="screen_media_picker_error_failed_selection">"Error al seleccionar archivos multimedia, por favor inténtalo de nuevo."</string>
<string name="screen_media_upload_preview_caption_warning">"Es posible que las leyendas no sean visibles para las personas que usan aplicaciones más antiguas."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Error al procesar el contenido multimedia, por favor inténtalo de nuevo."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Error al subir el contenido multimedia, por favor inténtalo de nuevo."</string>
<string name="screen_pinned_timeline_empty_state_description">"Presiona sobre un mensaje y selecciona «%1$s» para incluirlo aquí."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Fija los mensajes importantes para que se puedan descubrir fácilmente"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -390,12 +390,6 @@ Kas sa oled kindel, et soovid jätkata?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Kustuta: %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Seadistused"</string>
<string name="screen_media_picker_error_failed_selection">"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."</string>
<string name="screen_media_upload_preview_caption_warning">"Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele."</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Faili üleslaadimine ei õnnestunud."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Meediafaili üleslaadimine ei õnnestunud. Palun proovi uuesti."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Maksimaalne lubatud failisuurus on %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Fail on üleslaadimiseks liiga suur"</string>
<string name="screen_pinned_timeline_empty_state_description">"Siia lisamiseks vajuta sõnumil ja vali „%1$s“."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Et olulisi sõnumeid oleks lihtsam leida, tõsta nad esile"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -325,7 +325,6 @@ Arrazoia: %1$s."</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Astindu erroreen berri emateko"</string>
<string name="screen_media_picker_error_failed_selection">"Huts egin du multimedia aukeratzeak, saiatu berriro."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Huts egin du multimedia igotzeak, saiatu berriro."</string>
<string name="screen_pinned_timeline_screen_title_empty">"Finkatutako mezuak"</string>
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"Bidali mezua hala ere"</string>
<string name="screen_room_pinned_banner_indicator">"%2$s(e)tik %1$s"</string>

View file

@ -334,8 +334,6 @@
<string name="screen_create_poll_options_section_title">"گزینه‌ها"</string>
<string name="screen_create_poll_settings_section_title">"تنظیمات"</string>
<string name="screen_media_picker_error_failed_selection">"گزینش رسانه شکست خورد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_media_upload_preview_error_failed_processing">"پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_media_upload_preview_error_failed_sending">"بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_pinned_timeline_screen_title_empty">"پیام‌های سنجاق شده"</string>
<string name="screen_reset_identity_confirmation_subtitle">"داردید برای بازنشانی هویتتان به حساب %1$s می‌روید. پس از آن به کاره برگردانده خواهید شد."</string>
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"فرستادن پیام به هر روی"</string>

View file

@ -390,12 +390,6 @@ Haluatko varmasti jatkaa?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Poista %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Asetukset"</string>
<string name="screen_media_picker_error_failed_selection">"Median valinta epäonnistui, yritä uudelleen."</string>
<string name="screen_media_upload_preview_caption_warning">"Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia."</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Tiedostoa ei voitu lähettää."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Median käsittely epäonnistui, yritä uudelleen."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Median lähettäminen epäonnistui, yritä uudelleen."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Suurin sallittu tiedostokoko on %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Tiedosto on liian suuri lähetettäväksi"</string>
<string name="screen_pinned_timeline_empty_state_description">"Paina viestiä ja valitse “%1$s” lisätäksesi sen tänne."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Kiinnitä tärkeät viestit, jotta ne löytyvät helposti."</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -380,12 +380,6 @@ Raison : %1$s."</string>
<string name="screen_create_poll_remove_accessibility_label">"Supprimer %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Paramètres"</string>
<string name="screen_media_picker_error_failed_selection">"Échec de la sélection du média, veuillez réessayer."</string>
<string name="screen_media_upload_preview_caption_warning">"Les légendes peuvent ne pas être visibles pour les utilisateurs danciennes applications."</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Le fichier na pas pu être envoyé."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Échec du téléchargement du média, veuillez réessayer."</string>
<string name="screen_media_upload_preview_error_too_large_message">"La taille maximale autorisée pour les fichiers est de %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Le fichier est trop volumineux pour être envoyé."</string>
<string name="screen_pinned_timeline_empty_state_description">"Cliquez (clic long) sur un message et choisissez « %1$s » pour quil apparaisse ici."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Épinglez les messages importants pour leur donner plus de visibilité"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -402,15 +402,6 @@ Biztos, hogy folytatja?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Eltávolítás: %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Beállítások"</string>
<string name="screen_media_picker_error_failed_selection">"Nem sikerült kiválasztani a médiát, próbálja újra."</string>
<string name="screen_media_upload_preview_caption_warning">"Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Koppintson a feltöltött videók minőségének módosításához"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"A fájl nem tölthető fel."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Nem sikerült a média feltöltése, próbálja újra."</string>
<string name="screen_media_upload_preview_error_too_large_message">"A maximálisan megengedett fájlméret: %1$s ."</string>
<string name="screen_media_upload_preview_error_too_large_title">"A fájl túl nagy a feltöltéshez"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Képminőség optimalizációja"</string>
<string name="screen_media_upload_preview_processing">"Feldolgozás…"</string>
<string name="screen_pinned_timeline_empty_state_description">"Nyomjon hosszan az üzenetre, és válassza a „%1$s” lehetőséget, hogy itt szerepeljen."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Tűzze ki a fontos üzeneteket, hogy könnyen felfedezhetők legyenek"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -360,9 +360,6 @@ Apakah Anda yakin ingin melanjutkan?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Hapus %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Pengaturan"</string>
<string name="screen_media_picker_error_failed_selection">"Gagal memilih media, silakan coba lagi."</string>
<string name="screen_media_upload_preview_caption_warning">"Keterangan mungkin tidak terlihat oleh orang yang menggunakan aplikasi lama."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Gagal memproses media untuk diunggah, silakan coba lagi."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Gagal mengunggah media, silakan coba lagi."</string>
<string name="screen_pinned_timeline_empty_state_description">"Tekan pesan dan pilih “%1$s” untuk disertakan di sini."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Sematkan pesan penting agar mudah ditemukan"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -361,9 +361,6 @@ Sei sicuro di voler continuare?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Rimuovi %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Impostazioni"</string>
<string name="screen_media_picker_error_failed_selection">"Selezione del file multimediale fallita, riprova."</string>
<string name="screen_media_upload_preview_caption_warning">"Le didascalie potrebbero non essere visibili agli utenti di app meno recenti."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Elaborazione del file multimediale da caricare fallita, riprova."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Caricamento del file multimediale fallito, riprova."</string>
<string name="screen_pinned_timeline_empty_state_description">"Premi su un messaggio e scegli “%1$s” per includerlo qui."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Fissa i messaggi importanti così che possano essere trovati facilmente"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -253,8 +253,6 @@
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"შეცდომის შესატყობინებლად ტელეფონის შენჯღრევა"</string>
<string name="screen_media_picker_error_failed_selection">"მედიის შერჩევა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_media_upload_preview_error_failed_processing">"მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_media_upload_preview_error_failed_sending">"მედიის ატვირთვა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_room_error_failed_processing_media">"მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_room_error_failed_retrieving_user_details">"მომხმარებლის მონაცემების მოძიება ვერ მოხერხდა"</string>
<string name="screen_share_location_title">"მდებარეობის გაზიარება"</string>

View file

@ -142,8 +142,6 @@
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Papurtykite, kad praneštumėte apie klaidą"</string>
<string name="screen_media_picker_error_failed_selection">"Nepavyko pasirinkti laikmenos, pabandykite dar kartą."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Nepavyko įkelti laikmenos, pabandykite dar kartą."</string>
<string name="screen_room_error_failed_processing_media">"Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą."</string>
<string name="screen_room_error_failed_retrieving_user_details">"Nepavyko gauti naudotojo išsamios informacijos."</string>
<string name="settings_version_number">"Versija: %1$s (%2$s)"</string>

View file

@ -362,9 +362,6 @@ Er du sikker på at du vil fortsette?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Fjern %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Innstillinger"</string>
<string name="screen_media_picker_error_failed_selection">"Kunne ikke velge medium, prøv igjen."</string>
<string name="screen_media_upload_preview_caption_warning">"Teksting er kanskje ikke synlig for personer som bruker eldre apper."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Kunne ikke behandle medier for opplasting, vennligst prøv igjen."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Opplasting av medier mislyktes, vennligst prøv igjen."</string>
<string name="screen_pinned_timeline_empty_state_description">"Trykk på en melding og velg “%1$s” for å inkludere her."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Fest viktige meldinger slik at de lett kan ses"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -311,8 +311,6 @@ Reden: %1$s."</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Schudden om een bug te melden"</string>
<string name="screen_media_picker_error_failed_selection">"Het selecteren van media is mislukt. Probeer het opnieuw."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Het uploaden van media is mislukt. Probeer het opnieuw."</string>
<string name="screen_pinned_timeline_empty_state_description">"Druk op een bericht en kies „%1$s” om het hier toe te voegen."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Zet belangrijke berichten vast zodat ze gemakkelijk te vinden zijn"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -386,12 +386,6 @@ Czy na pewno chcesz kontynuować?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Usuń %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Ustawienia"</string>
<string name="screen_media_picker_error_failed_selection">"Nie udało się wybrać multimediów. Spróbuj ponownie."</string>
<string name="screen_media_upload_preview_caption_warning">"Opis może być niedostępny dla osób korzystających ze starszej wersji aplikacji."</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Nie udało się przesłać pliku."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Przesyłanie multimediów nie powiodło się, spróbuj ponownie."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Maksymalny dozwolony rozmiar pliku to %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Plik jest za duży, aby go przesłać."</string>
<string name="screen_pinned_timeline_empty_state_description">"Naciśnij wiadomość i wybierz “%1$s”, aby dołączyć tutaj."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Przypinaj ważne wiadomości, aby można było je łatwo znaleźć"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -361,9 +361,6 @@ Você tem certeza de que deseja continuar?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Remover %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Configurações"</string>
<string name="screen_media_picker_error_failed_selection">"Falha ao selecionar a mídia, tente novamente."</string>
<string name="screen_media_upload_preview_caption_warning">"As legendas podem não ser visíveis para pessoas que usam aplicativos mais antigos."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Falha ao processar mídia para upload. Tente novamente."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Falha ao enviar mídia. Tente novamente."</string>
<string name="screen_pinned_timeline_empty_state_description">"Pressione em uma mensagem e escolha \"%1$s\" para incluir aqui."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Fixe mensagens importantes para que elas possam ser facilmente descobertas"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -390,12 +390,6 @@ Tens a certeza de que queres continuar?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Remover %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Configurações"</string>
<string name="screen_media_picker_error_failed_selection">"Falha ao selecionar multimédia, por favor tente novamente."</string>
<string name="screen_media_upload_preview_caption_warning">"As legendas poderão não ser visíveis em versões mais antigas da aplicação."</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Não foi possível enviar o ficheiro"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Falha ao processar multimédia para carregamento, por favor tente novamente."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Falhar ao carregar multimédia, por favor tente novamente."</string>
<string name="screen_media_upload_preview_error_too_large_message">"O tamanho máximo permitido é %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"O ficheiro é demasiado grande para enviar"</string>
<string name="screen_pinned_timeline_empty_state_description">"Pressione uma mensagem e escolha \"%1$s\" para incluir aqui."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Fixa mensagens importantes para que possam ser facilmente descobertas"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -302,8 +302,6 @@ Motiv:%1$s."</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Rageshake pentru a raporta erori"</string>
<string name="screen_media_picker_error_failed_selection">"Selectarea fișierelor media a eșuat, încercați din nou."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Procesarea datelor media a eșuat, vă rugăm să încercați din nou."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Încărcarea fișierelor media a eșuat, încercați din nou."</string>
<string name="screen_pinned_timeline_empty_state_description">"Apăsați pe un mesaj și alegeți \"%1$s\" pentru a-l include aici."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Fixați mesajele importante, astfel încât să poată fi descoperite cu ușurință"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -351,9 +351,6 @@
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Встряхните устройство, чтобы сообщить об ошибке"</string>
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать носитель, попробуйте еще раз."</string>
<string name="screen_media_upload_preview_caption_warning">"Подпись может быть не видна пользователям старых приложений."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Не удалось загрузить медиафайлы, попробуйте еще раз."</string>
<string name="screen_pinned_timeline_empty_state_description">"Нажмите на сообщение и выберите “%1$s”, чтобы добавить его сюда."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Закрепите важные сообщения, чтобы их можно было легко найти"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -411,15 +411,6 @@ Naozaj chcete pokračovať?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Odstrániť %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Nastavenia"</string>
<string name="screen_media_picker_error_failed_selection">"Nepodarilo sa vybrať médium, skúste to prosím znova."</string>
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Ťuknutím zmeníte kvalitu nahratého videa"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Súbor sa nepodarilo nahrať."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Nepodarilo sa nahrať médiá, skúste to prosím znova."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Maximálna povolená veľkosť súboru je %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Súbor je príliš veľký na nahratie"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimalizovať kvalitu obrázku"</string>
<string name="screen_media_upload_preview_processing">"Prebieha spracovanie…"</string>
<string name="screen_pinned_timeline_empty_state_description">"Stlačte správu a vyberte možnosť „%1$s“, ktorú chcete zahrnúť sem."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Pripnite dôležité správy, aby sa dali ľahko nájsť"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -373,9 +373,6 @@ Anledning:%1$s."</string>
<string name="screen_create_poll_remove_accessibility_label">"Ta bort %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Inställningar"</string>
<string name="screen_media_picker_error_failed_selection">"Misslyckades att välja media, vänligen pröva igen."</string>
<string name="screen_media_upload_preview_caption_warning">"Bildtexter kanske inte är synliga för personer som använder äldre appar."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Misslyckades att ladda upp media, vänligen pröva igen."</string>
<string name="screen_pinned_timeline_empty_state_description">"Tryck på ett meddelande och välj ”%1$s” för att inkludera det här."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Fäst viktiga meddelanden så att de lätt kan upptäckas"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -323,9 +323,6 @@ Neden: %1$s."</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Hata bildirmek için Rageshake"</string>
<string name="screen_media_picker_error_failed_selection">"Medya seçilemedi, lütfen tekrar deneyin."</string>
<string name="screen_media_upload_preview_caption_warning">"Açıklamalar, eski uygulamaları kullanan kişiler tarafından görülemeyebilir."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Medya yüklenemedi, lütfen tekrar deneyin."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Medya yüklenemedi, lütfen tekrar deneyin."</string>
<string name="screen_pinned_timeline_empty_state_description">"Bir mesaja basın ve buraya eklemek için “%1$s” yi seçin."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Önemli mesajları kolayca keşfedilebilmeleri için sabitleyin"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -385,12 +385,6 @@
<string name="screen_create_poll_remove_accessibility_label">"Вилучити %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Налаштування"</string>
<string name="screen_media_picker_error_failed_selection">"Не вдалося вибрати медіафайл, спробуйте ще раз."</string>
<string name="screen_media_upload_preview_caption_warning">"Користувачі старих застосунків можуть не бачити підписи."</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Файл не може бути вивантажено."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Не вдалося завантажити медіафайл, спробуйте ще раз."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Максимально дозволений розмір файлу — %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Файл завеликий для вивантаження"</string>
<string name="screen_pinned_timeline_empty_state_description">"Натисніть на повідомлення і виберіть \"%1$s\", щоб додати його сюди."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Закріпіть важливі повідомлення, щоб їх можна було легко знайти"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -274,8 +274,6 @@
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"خطاء کی اطلاع دینے کیلئے غصے سے ہلائیں"</string>
<string name="screen_media_picker_error_failed_selection">"وسائط منتخب کرنا ناکام، برائے مہربانی دوبارہ کوشش کریں۔"</string>
<string name="screen_media_upload_preview_error_failed_processing">"وسائط کا معالجہ برائے ترفیع ناکام، برائے مہربانی دوبارہ کوشش کریں۔"</string>
<string name="screen_media_upload_preview_error_failed_sending">"وسائط رفع کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔"</string>
<plurals name="screen_pinned_timeline_screen_title">
<item quantity="one">"%1$d مثبوتہ پیغام"</item>
<item quantity="other">"%1$d مثبوتہ پیغامات"</item>

View file

@ -191,8 +191,6 @@
<string name="login_initial_device_name_android">"%1$sAndroid"</string>
<string name="preference_rageshake">"Xato haqida xabar berish uchun G\'azablanish"</string>
<string name="screen_media_picker_error_failed_selection">"Media tanlash jarayonida xatolik yuz berdi, qayta urinib ko\'ring"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Mediani yuklab bolmadi, qayta urinib koring."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Media yuklanmadi, qayta urinib koring."</string>
<string name="screen_room_error_failed_processing_media">"Mediani yuklab bolmadi, qayta urinib koring."</string>
<string name="screen_room_error_failed_retrieving_user_details">"Foydalanuvchi tafsilotlarini olinmadi"</string>
<string name="screen_share_location_title">"Joylashuvni ulashish"</string>

View file

@ -341,9 +341,6 @@
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"憤怒搖晃以回報臭蟲"</string>
<string name="screen_media_picker_error_failed_selection">"選取媒體失敗,請再試一次。"</string>
<string name="screen_media_upload_preview_caption_warning">"使用舊應用程式的使用者可能看不到標題。"</string>
<string name="screen_media_upload_preview_error_failed_processing">"無法處理要上傳的媒體,請再試一次。"</string>
<string name="screen_media_upload_preview_error_failed_sending">"無法上傳媒體檔案,請稍後再試。"</string>
<string name="screen_pinned_timeline_empty_state_description">"按一下訊息,然後選擇「%1$s」以加入至此。"</string>
<string name="screen_pinned_timeline_empty_state_headline">"釘選重要訊息,如此才能輕鬆發現"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -344,9 +344,6 @@
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"摇一摇以报错"</string>
<string name="screen_media_picker_error_failed_selection">"选择媒体失败,请重试。"</string>
<string name="screen_media_upload_preview_caption_warning">"使用旧版应用程序的用户可能无法看到字幕。"</string>
<string name="screen_media_upload_preview_error_failed_processing">"处理要上传的媒体失败,请重试。"</string>
<string name="screen_media_upload_preview_error_failed_sending">"上传媒体失败,请重试。"</string>
<string name="screen_pinned_timeline_empty_state_description">"按下消息并选择 “%1$s” 将其包含在此处。"</string>
<string name="screen_pinned_timeline_empty_state_headline">"固定重要消息,以便轻松发现它们"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -403,15 +403,6 @@ Are you sure you want to continue?"</string>
<string name="screen_create_poll_remove_accessibility_label">"Remove %1$s"</string>
<string name="screen_create_poll_settings_section_title">"Settings"</string>
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
<string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tap to change the video upload quality"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"The file could not be uploaded."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
<string name="screen_media_upload_preview_error_too_large_message">"The maximum file size allowed is %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"The file is too large to upload"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimise image quality"</string>
<string name="screen_media_upload_preview_processing">"Processing…"</string>
<string name="screen_pinned_timeline_empty_state_description">"Press on a message and choose “%1$s” to include here."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Pin important messages so that they can be easily discovered"</string>
<plurals name="screen_pinned_timeline_screen_title">

View file

@ -13,6 +13,9 @@ android {
namespace = "io.element.android.libraries.ui.utils"
dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.services.toolbox.impl)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.coroutines.test)

View file

@ -0,0 +1,24 @@
/*
* 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.ui.utils.formatter
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import io.element.android.libraries.androidutils.filesize.AndroidFileSizeFormatter
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.ui.utils.version.LocalSdkIntVersionProvider
@Composable
fun rememberFileSizeFormatter(): FileSizeFormatter {
val context = LocalContext.current
val sdkIntProvider = LocalSdkIntVersionProvider.current
return remember {
AndroidFileSizeFormatter(context, sdkIntProvider)
}
}

View file

@ -0,0 +1,14 @@
/*
* 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.ui.utils.version
import androidx.compose.runtime.staticCompositionLocalOf
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import io.element.android.services.toolbox.impl.sdk.DefaultBuildVersionSdkIntProvider
val LocalSdkIntVersionProvider = staticCompositionLocalOf<BuildVersionSdkIntProvider> { DefaultBuildVersionSdkIntProvider() }