diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 2974b99696..5ffd90b3ab 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.utils.TextPillificationHelper import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -110,6 +111,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import kotlin.time.Duration.Companion.seconds import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes @@ -145,6 +147,7 @@ class MessageComposerPresenter( private val featureFlagService: FeatureFlagService, private val contentScannerService: ContentScannerService, private val contentValidationCache: EventContentValidationCache, + private val dispatchers: CoroutineDispatchers, ) : Presenter { @AssistedFactory interface Factory { @@ -193,22 +196,22 @@ class MessageComposerPresenter( .collectAsState(initial = false) val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType -> - handlePickedMedia(uri, mimeType) + localCoroutineScope.handlePickedMedia(uri, mimeType) } val galleryMultiMediaPicker = mediaPickerProvider.registerGalleryMultiPicker { uris -> - handlePickedMediaList(uris) + localCoroutineScope.handlePickedMediaList(uris) } val filesPicker = mediaPickerProvider.registerFileMultiPicker(AnyMimeTypes) { uris -> - handlePickedMediaList(uris, sendAsFile = true) + localCoroutineScope.handlePickedMediaList(uris, sendAsFile = true) } val fileSinglePicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType -> - handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true) + localCoroutineScope.handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true) } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri -> - handlePickedMedia(uri, MimeTypes.Jpeg) + localCoroutineScope.handlePickedMedia(uri, MimeTypes.Jpeg) } val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri -> - handlePickedMedia(uri, MimeTypes.Mp4) + localCoroutineScope.handlePickedMedia(uri, MimeTypes.Mp4) } val isFullScreen = rememberSaveable { mutableStateOf(false) @@ -286,17 +289,16 @@ class MessageComposerPresenter( } is MessageComposerEvent.SendUri -> { val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId - sessionCoroutineScope.sendAttachment( - attachment = Attachment.Media( - localMedia = localMediaFactory.createFromUri( - uri = event.uri, - mimeType = null, - name = null, - formattedFileSize = null - ), - ), - inReplyToEventId = inReplyToEventId, - ) + sessionCoroutineScope.launch { + val localMedia = createLocalMedia( + uri = event.uri, + mimeType = null, + ) + sendAttachment( + attachment = Attachment.Media(localMedia), + inReplyToEventId = inReplyToEventId, + ) + } resetComposerModeAfterAttaching() } @@ -640,17 +642,28 @@ class MessageComposerPresenter( } } - private fun handlePickedMedia( - uri: Uri?, - mimeType: String? = null, - sendAsFile: Boolean = false, - ) { - uri ?: return - val localMedia = localMediaFactory.createFromUri( + // Creating the LocalMedia queries the ContentResolver and can decode the media, so do it off the main thread + private suspend fun createLocalMedia( + uri: Uri, + mimeType: String?, + ) = withContext(dispatchers.io) { + localMediaFactory.createFromUri( uri = uri, mimeType = mimeType, name = null, - formattedFileSize = null + formattedFileSize = null, + ) + } + + private fun CoroutineScope.handlePickedMedia( + uri: Uri?, + mimeType: String? = null, + sendAsFile: Boolean = false, + ) = launch { + uri ?: return@launch + val localMedia = createLocalMedia( + uri = uri, + mimeType = mimeType, ) val mediaAttachment = Attachment.Media(localMedia, sendAsFile = sendAsFile) val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId @@ -659,7 +672,7 @@ class MessageComposerPresenter( resetComposerModeAfterAttaching() } - private fun handlePickedMediaList( + private fun CoroutineScope.handlePickedMediaList( uris: List, sendAsFile: Boolean = false, ) { @@ -668,19 +681,19 @@ class MessageComposerPresenter( handlePickedMedia(uris.first(), sendAsFile = sendAsFile) return } - val attachments = uris.map { uri -> - val localMedia = localMediaFactory.createFromUri( - uri = uri, - mimeType = null, - name = null, - formattedFileSize = null, - ) - Attachment.Media(localMedia, sendAsFile = sendAsFile) - }.toImmutableList() - val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId - navigator.navigateToPreviewAttachments(attachments, inReplyToEventId) + launch { + val attachments = uris.map { uri -> + val localMedia = createLocalMedia( + uri = uri, + mimeType = null, + ) + Attachment.Media(localMedia, sendAsFile = sendAsFile) + }.toImmutableList() + val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId + navigator.navigateToPreviewAttachments(attachments, inReplyToEventId) - resetComposerModeAfterAttaching() + resetComposerModeAfterAttaching() + } } private fun resetComposerModeAfterAttaching() { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt index a8f6c338f5..4535607cb6 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt @@ -324,6 +324,7 @@ class MessageComposerPresenterSlashCommandTest { featureFlagService = featureFlagService, contentScannerService = { _, _ -> }, contentValidationCache = InMemoryEventContentValidationCache(), + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), ).apply { isTesting = true showTextFormatting = isRichTextEditorEnabled diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index 3253e6088d..74b8047449 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -1723,6 +1723,7 @@ class MessageComposerPresenterTest : RobolectricTest() { featureFlagService = featureFlagService, contentScannerService = { _, _ -> }, contentValidationCache = InMemoryEventContentValidationCache(), + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), ).apply { isTesting = true showTextFormatting = isRichTextEditorEnabled