Allow replying to a message with an attachment (#5261)

This commit is contained in:
Jorge Martin Espinosa 2025-09-05 17:36:54 +02:00 committed by GitHub
parent 670c929993
commit a8a6a51953
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 57 additions and 34 deletions

View file

@ -142,7 +142,7 @@ class MessagesFlowNode(
) : NavTarget
@Parcelize
data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment) : NavTarget
data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment, val inReplyToEventId: EventId?) : NavTarget
@Parcelize
data class LocationViewer(val location: Location, val description: String?) : NavTarget
@ -224,10 +224,11 @@ class MessagesFlowNode(
)
}
override fun onPreviewAttachments(attachments: ImmutableList<Attachment>) {
override fun onPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
backstack.push(NavTarget.AttachmentPreview(
attachment = attachments.first(),
timelineMode = Timeline.Mode.Live,
inReplyToEventId = inReplyToEventId,
))
}
@ -314,6 +315,7 @@ class MessagesFlowNode(
val inputs = AttachmentsPreviewNode.Inputs(
attachment = navTarget.attachment,
timelineMode = navTarget.timelineMode,
inReplyToEventId = navTarget.inReplyToEventId,
)
createNode<AttachmentsPreviewNode>(buildContext, listOf(inputs))
}
@ -416,10 +418,11 @@ class MessagesFlowNode(
)
}
override fun onPreviewAttachments(attachments: ImmutableList<Attachment>) {
override fun onPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
backstack.push(NavTarget.AttachmentPreview(
attachment = attachments.first(),
timelineMode = Timeline.Mode.Thread(navTarget.threadRootId)
timelineMode = Timeline.Mode.Thread(navTarget.threadRootId),
inReplyToEventId = inReplyToEventId,
))
}

View file

@ -20,7 +20,7 @@ interface MessagesNavigator {
fun onForwardEventClick(eventId: EventId)
fun onReportContentClick(eventId: EventId, senderId: UserId)
fun onEditPollClick(eventId: EventId)
fun onPreviewAttachment(attachments: ImmutableList<Attachment>)
fun onPreviewAttachment(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>)
fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?)
}

View file

@ -112,7 +112,7 @@ class MessagesNode(
interface Callback : Plugin {
fun onRoomDetailsClick()
fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
fun onPreviewAttachments(attachments: ImmutableList<Attachment>)
fun onPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
fun onUserDataClick(userId: UserId)
fun onPermalinkClick(data: PermalinkData)
fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
@ -219,8 +219,8 @@ class MessagesNode(
callbacks.forEach { it.onEditPollClick(eventId) }
}
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>) {
callbacks.forEach { it.onPreviewAttachments(attachments) }
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
callbacks.forEach { it.onPreviewAttachments(attachments, inReplyToEventId) }
}
override fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>) {

View file

@ -20,6 +20,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
@ -34,6 +35,7 @@ class AttachmentsPreviewNode(
data class Inputs(
val attachment: Attachment,
val timelineMode: Timeline.Mode,
val inReplyToEventId: EventId?,
) : NodeInputs
private val inputs: Inputs = inputs()
@ -46,6 +48,7 @@ class AttachmentsPreviewNode(
attachment = inputs.attachment,
timelineMode = inputs.timelineMode,
onDoneListener = onDoneListener,
inReplyToEventId = inputs.inReplyToEventId,
)
@Composable

View file

@ -54,6 +54,7 @@ class AttachmentsPreviewPresenter(
@Assisted private val attachment: Attachment,
@Assisted private val onDoneListener: OnDoneListener,
@Assisted private val timelineMode: Timeline.Mode,
@Assisted private val inReplyToEventId: EventId?,
mediaSenderFactory: MediaSender.Factory,
private val permalinkBuilder: PermalinkBuilder,
private val temporaryUriDeleter: TemporaryUriDeleter,
@ -67,6 +68,7 @@ class AttachmentsPreviewPresenter(
attachment: Attachment,
timelineMode: Timeline.Mode,
onDoneListener: OnDoneListener,
inReplyToEventId: EventId?,
): AttachmentsPreviewPresenter
}
@ -182,7 +184,7 @@ class AttachmentsPreviewPresenter(
caption = caption,
sendActionState = sendActionState,
dismissAfterSend = false,
inReplyToEventId = null,
inReplyToEventId = inReplyToEventId,
)
// Clean up the pre-processed media after it's been sent

View file

@ -45,6 +45,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
@ -54,12 +55,10 @@ import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
import io.element.android.libraries.matrix.api.room.getDirectRoomMember
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.map
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaSender
@ -247,16 +246,23 @@ class MessageComposerPresenter(
richTextEditorState = richTextEditorState,
)
}
is MessageComposerEvents.SendUri -> sessionCoroutineScope.sendAttachment(
attachment = Attachment.Media(
localMedia = localMediaFactory.createFromUri(
uri = event.uri,
mimeType = null,
name = null,
formattedFileSize = null
is MessageComposerEvents.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,
)
// Reset composer since the attachment has been sent
messageComposerContext.composerMode = MessageComposerMode.Normal
}
is MessageComposerEvents.SetMode -> {
localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState)
}
@ -497,12 +503,14 @@ class MessageComposerPresenter(
private fun CoroutineScope.sendAttachment(
attachment: Attachment,
inReplyToEventId: EventId?,
) = when (attachment) {
is Attachment.Media -> {
launch {
sendMedia(
uri = attachment.localMedia.uri,
mimeType = attachment.localMedia.info.mimeType,
inReplyToEventId = inReplyToEventId,
)
}
}
@ -521,17 +529,23 @@ class MessageComposerPresenter(
formattedFileSize = null
)
val mediaAttachment = Attachment.Media(localMedia)
navigator.onPreviewAttachment(persistentListOf(mediaAttachment))
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
navigator.onPreviewAttachment(persistentListOf(mediaAttachment), inReplyToEventId)
// Reset composer since the attachment will be sent in a separate flow
messageComposerContext.composerMode = MessageComposerMode.Normal
}
private suspend fun sendMedia(
uri: Uri,
mimeType: String,
inReplyToEventId: EventId?,
) = runCatchingExceptions {
mediaSender.sendMedia(
uri = uri,
mimeType = mimeType,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
inReplyToEventId = inReplyToEventId,
).getOrThrow()
}
.onFailure { cause ->

View file

@ -115,7 +115,7 @@ class ThreadedMessagesNode(
interface Callback : Plugin {
fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
fun onPreviewAttachments(attachments: ImmutableList<Attachment>)
fun onPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
fun onUserDataClick(userId: UserId)
fun onPermalinkClick(data: PermalinkData)
fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
@ -215,8 +215,8 @@ class ThreadedMessagesNode(
callbacks.forEach { it.onEditPollClick(eventId) }
}
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>) {
callbacks.forEach { it.onPreviewAttachments(attachments) }
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
callbacks.forEach { it.onPreviewAttachments(attachments, inReplyToEventId) }
}
override fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>) = Unit

View file

@ -21,7 +21,7 @@ class FakeMessagesNavigator(
private val onForwardEventClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
private val onReportContentClickLambda: (eventId: EventId, senderId: UserId) -> Unit = { _, _ -> lambdaError() },
private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
private val onPreviewAttachmentLambda: (attachments: ImmutableList<Attachment>) -> Unit = { _ -> lambdaError() },
private val onPreviewAttachmentLambda: (attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) -> Unit = { _, _ -> lambdaError() },
private val onNavigateToRoomLambda: (roomId: RoomId, serverNames: List<String>) -> Unit = { _, _ -> lambdaError() },
private val onOpenThreadLambda: (threadRootId: ThreadId, focusedEventId: EventId?) -> Unit = { _, _ -> lambdaError() },
) : MessagesNavigator {
@ -41,8 +41,8 @@ class FakeMessagesNavigator(
onEditPollClickLambda(eventId)
}
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>) {
onPreviewAttachmentLambda(attachments)
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
onPreviewAttachmentLambda(attachments, inReplyToEventId)
}
override fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>) {

View file

@ -618,6 +618,7 @@ class AttachmentsPreviewPresenterTest {
dispatchers = testCoroutineDispatchers(),
mediaOptimizationSelectorPresenterFactory = mediaOptimizationSelectorPresenterFactory,
timelineMode = timelineMode,
inReplyToEventId = null,
)
}

View file

@ -688,7 +688,7 @@ class MessageComposerPresenterTest {
val room = FakeJoinedRoom(
typingNoticeResult = { Result.success(Unit) }
)
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
@ -728,7 +728,7 @@ class MessageComposerPresenterTest {
val room = FakeJoinedRoom(
typingNoticeResult = { Result.success(Unit) }
)
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
@ -785,7 +785,7 @@ class MessageComposerPresenterTest {
val room = FakeJoinedRoom(
typingNoticeResult = { Result.success(Unit) }
)
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
@ -846,7 +846,7 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
@ -870,7 +870,7 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter()
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
@ -896,7 +896,7 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
@ -920,7 +920,7 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter()
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment>, _: EventId? -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)