Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,7 @@ class MessageComposerPresenter(
inReplyToEventId = inReplyToEventId,
)

// Reset composer since the attachment has been sent
messageComposerContext.composerMode = MessageComposerMode.Normal
resetComposerModeAfterAttaching()
}
is MessageComposerEvent.SetMode -> {
localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState)
Expand Down Expand Up @@ -638,8 +637,7 @@ class MessageComposerPresenter(
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
navigator.navigateToPreviewAttachments(persistentListOf(mediaAttachment), inReplyToEventId)

// Reset composer since the attachment will be sent in a separate flow
messageComposerContext.composerMode = MessageComposerMode.Normal
resetComposerModeAfterAttaching()
}

private fun handlePickedMediaList(
Expand All @@ -663,7 +661,15 @@ class MessageComposerPresenter(
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
navigator.navigateToPreviewAttachments(attachments, inReplyToEventId)

messageComposerContext.composerMode = MessageComposerMode.Normal
resetComposerModeAfterAttaching()
}

private fun resetComposerModeAfterAttaching() {
// An attachment is sent as its own message, so a Reply/Normal mode is consumed and reset.
// An in-progress edit must survive: the typed text still edits the original on the next send.
if (!messageComposerContext.composerMode.isEditing) {
messageComposerContext.composerMode = MessageComposerMode.Normal
}
}

private suspend fun sendMedia(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import io.element.android.features.messages.impl.utils.TextPillificationHelper
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
Expand Down Expand Up @@ -758,6 +759,79 @@ class MessageComposerPresenterTest : RobolectricTest() {
}
}

@Test
fun `present - Pick media from gallery while editing keeps the edit pending`() = runTest {
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(navigator = navigator)
pickerProvider.givenMimeType(MimeTypes.Images)
presenter.test {
var state = awaitFirstItem()
val editMode = anEditMode()
state.eventSink(MessageComposerEvent.SetMode(editMode))
state = awaitItem()
assertThat(state.mode).isEqualTo(editMode)
// The media is sent as a new message while the edit stays active. If the mode reset,
// a Normal-mode state would be emitted and expectNoEvents() below would fail.
state.eventSink(MessageComposerEvent.PickAttachmentSource.FromGallery)
onPreviewAttachmentLambda.assertions().isCalledOnce()
expectNoEvents()
assertThat(state.mode).isEqualTo(editMode)
}
}

@Test
fun `present - Pick media from gallery while replying clears the reply mode`() = runTest {
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(navigator = navigator)
pickerProvider.givenMimeType(MimeTypes.Images)
presenter.test {
var state = awaitFirstItem()
state.eventSink(MessageComposerEvent.SetMode(aReplyMode()))
state = awaitItem()
assertThat(state.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
// The media becomes the reply, so the reply intent is consumed and the composer resets.
state.eventSink(MessageComposerEvent.PickAttachmentSource.FromGallery)
onPreviewAttachmentLambda.assertions().isCalledOnce()
state = awaitItem()
assertThat(state.mode).isEqualTo(MessageComposerMode.Normal)
}
}

@Test
fun `present - Pick multiple media from gallery while editing keeps the edit pending`() = runTest {
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(
navigator = navigator,
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.SendGalleryMessages.key to true)
),
)
pickerProvider.givenMimeType(MimeTypes.Images)
// Two Uris take the multi-item branch (handlePickedMediaList) instead of delegating to the
// single-item path, so the edit is preserved for gallery messages too.
pickerProvider.givenMultipleResults(listOf(mockk(), mockk()))
presenter.test {
var state = awaitFirstItem()
val editMode = anEditMode()
state.eventSink(MessageComposerEvent.SetMode(editMode))
state = awaitItem()
assertThat(state.mode).isEqualTo(editMode)
state.eventSink(MessageComposerEvent.PickAttachmentSource.FromGallery)
onPreviewAttachmentLambda.assertions().isCalledOnce()
expectNoEvents()
assertThat(state.mode).isEqualTo(editMode)
}
}

@Test
fun `present - Pick media from gallery & cancel does nothing`() = runTest {
val presenter = createPresenter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import io.element.android.libraries.mediapickers.api.PickerProvider
class FakePickerProvider : PickerProvider {
private var mimeType = MimeTypes.Any
private var result: Uri? = null
private var multipleResults: List<Uri>? = null

@Composable
override fun registerGalleryPicker(onResult: (uri: Uri?, mimeType: String?) -> Unit): PickerLauncher<PickVisualMediaRequest, Uri?> {
Expand All @@ -32,7 +33,7 @@ class FakePickerProvider : PickerProvider {

@Composable
override fun registerGalleryMultiPicker(onResult: (uris: List<Uri>) -> Unit): PickerLauncher<PickVisualMediaRequest, List<Uri>> {
return NoOpPickerLauncher { onResult(result?.let { listOf(it) } ?: emptyList()) }
return NoOpPickerLauncher { onResult(multipleResults ?: result?.let { listOf(it) } ?: emptyList()) }
}

@Composable
Expand All @@ -42,7 +43,7 @@ class FakePickerProvider : PickerProvider {

@Composable
override fun registerFileMultiPicker(mimeType: String, onResult: (uris: List<Uri>) -> Unit): PickerLauncher<Array<String>, List<Uri>> {
return NoOpPickerLauncher { onResult(result?.let { listOf(it) } ?: emptyList()) }
return NoOpPickerLauncher { onResult(multipleResults ?: result?.let { listOf(it) } ?: emptyList()) }
}

@Composable
Expand All @@ -59,6 +60,10 @@ class FakePickerProvider : PickerProvider {
this.result = value
}

fun givenMultipleResults(uris: List<Uri>) {
this.multipleResults = uris
}

fun givenMimeType(mimeType: String) {
this.mimeType = mimeType
}
Expand Down
Loading