diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt index af4ac6c9c7..ee12c0ee8c 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -379,7 +379,7 @@ class SendLocationPresenterTest { fakeMessageComposerContext.apply { composerMode = MessageComposerMode.Edit( eventId = null, - defaultContent = "", + content = "", transactionId = null ) } @@ -427,7 +427,7 @@ class SendLocationPresenterTest { fakeMessageComposerContext.apply { composerMode = MessageComposerMode.Edit( eventId = null, - defaultContent = "", + content = "", transactionId = null ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 065c81e407..8202360bbf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -47,21 +47,10 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.typing.TypingNotificationPresenter import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter @@ -80,12 +69,13 @@ import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarM import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MessageEventType -import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo -import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +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.canCall import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState @@ -98,6 +88,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber class MessagesPresenter @AssistedInject constructor( + @Assisted private val navigator: MessagesNavigator, private val room: MatrixRoom, private val composerPresenter: MessageComposerPresenter, private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, @@ -109,15 +100,15 @@ class MessagesPresenter @AssistedInject constructor( private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, - private val messageSummaryFormatter: MessageSummaryFormatter, private val dispatchers: CoroutineDispatchers, private val clipboardHelper: ClipboardHelper, private val featureFlagsService: FeatureFlagService, private val htmlConverterProvider: HtmlConverterProvider, - @Assisted private val navigator: MessagesNavigator, private val buildMeta: BuildMeta, private val timelineController: TimelineController, + private val permalinkParser: PermalinkParser, ) : Presenter { + private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator) @AssistedFactory @@ -340,66 +331,20 @@ class MessagesPresenter @AssistedInject constructor( } } - private fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) { + private suspend fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) { if (targetEvent.eventId == null) return - val textContent = messageSummaryFormatter.format(targetEvent) - val attachmentThumbnailInfo = when (targetEvent.content) { - is TimelineItemImageContent -> AttachmentThumbnailInfo( - thumbnailSource = targetEvent.content.thumbnailSource ?: targetEvent.content.mediaSource, - textContent = targetEvent.content.body, - type = AttachmentThumbnailType.Image, - blurHash = targetEvent.content.blurhash, - ) - is TimelineItemStickerContent -> AttachmentThumbnailInfo( - thumbnailSource = targetEvent.content.thumbnailSource ?: targetEvent.content.mediaSource, - textContent = targetEvent.content.body, - type = AttachmentThumbnailType.Image, - blurHash = targetEvent.content.blurhash, - ) - is TimelineItemVideoContent -> AttachmentThumbnailInfo( - thumbnailSource = targetEvent.content.thumbnailSource, - textContent = targetEvent.content.body, - type = AttachmentThumbnailType.Video, - blurHash = targetEvent.content.blurHash, - ) - is TimelineItemFileContent -> AttachmentThumbnailInfo( - thumbnailSource = targetEvent.content.thumbnailSource, - textContent = targetEvent.content.body, - type = AttachmentThumbnailType.File, - ) - is TimelineItemAudioContent -> AttachmentThumbnailInfo( - textContent = targetEvent.content.body, - type = AttachmentThumbnailType.Audio, - ) - is TimelineItemVoiceContent -> AttachmentThumbnailInfo( - textContent = textContent, - type = AttachmentThumbnailType.Voice, - ) - is TimelineItemLocationContent -> AttachmentThumbnailInfo( - type = AttachmentThumbnailType.Location, - ) - is TimelineItemPollContent -> AttachmentThumbnailInfo( - textContent = targetEvent.content.question, - type = AttachmentThumbnailType.Poll, - ) - is TimelineItemTextBasedContent, - is TimelineItemRedactedContent, - is TimelineItemStateContent, - is TimelineItemEncryptedContent, - is TimelineItemLegacyCallInviteContent, - is TimelineItemCallNotifyContent, - is TimelineItemUnknownContent -> null + timelineController.invokeOnCurrentTimeline { + loadReplyDetails(targetEvent.eventId) + .onSuccess { inReplyTo -> + val composerMode = MessageComposerMode.Reply( + inReplyTo.map(permalinkParser) + ) + composerState.eventSink( + MessageComposerEvents.SetMode(composerMode) + ) + } + .onFailure { Timber.e(it) } } - val composerMode = MessageComposerMode.Reply( - isThreaded = targetEvent.isThreaded, - senderName = targetEvent.safeSenderName, - eventId = targetEvent.eventId, - attachmentThumbnailInfo = attachmentThumbnailInfo, - defaultContent = textContent, - ) - composerState.eventSink( - MessageComposerEvents.SetMode(composerMode) - ) } private fun handleShowDebugInfoAction(event: TimelineItem.Event) { 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 48211267ed..87bc02911d 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 @@ -184,9 +184,9 @@ class MessageComposerPresenter @Inject constructor( when (val modeValue = messageComposerContext.composerMode) { is MessageComposerMode.Edit -> if (showTextFormatting) { - richTextEditorState.setHtml(modeValue.defaultContent) + richTextEditorState.setHtml(modeValue.content) } else { - markdownTextEditorState.text.update(modeValue.defaultContent, true) + markdownTextEditorState.text.update(modeValue.content, true) } else -> Unit } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 19df7b0d68..0c3218908b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -16,7 +16,6 @@ package io.element.android.features.messages.impl.timeline -import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.ReadReceiptData @@ -37,6 +36,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails +import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowDisambiguatedPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowDisambiguatedPreview.kt index e449f0beda..7357e279fc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowDisambiguatedPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowDisambiguatedPreview.kt @@ -20,8 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsDisambiguatedProvider @PreviewsDayNight @Composable @@ -33,18 +33,3 @@ internal fun TimelineItemEventRowDisambiguatedPreview( displayNameAmbiguous = true, ) } - -class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() { - override val values: Sequence - get() = sequenceOf( - aMessageContent( - body = "Message which are being replied.", - type = TextMessageType("Message which are being replied.", null) - ), - ).map { - aInReplyToDetails( - displayNameAmbiguous = true, - eventContent = it, - ) - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt index 7a97ab935a..cdb3ee1d1a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt @@ -20,9 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent -import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsInformativeProvider @PreviewsDayNight @Composable @@ -31,15 +30,3 @@ internal fun TimelineItemEventRowWithReplyInformativePreview( ) = ElementPreview { TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) } - -class InReplyToDetailsInformativeProvider : InReplyToDetailsProvider() { - override val values: Sequence - get() = sequenceOf( - RedactedContent, - UnableToDecryptContent(UnableToDecryptContent.Data.Unknown), - ).map { - aInReplyToDetails( - eventContent = it, - ) - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt index bd31103ff0..c07a7ee41c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt @@ -20,8 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsOtherProvider @PreviewsDayNight @Composable @@ -30,11 +30,3 @@ internal fun TimelineItemEventRowWithReplyOtherPreview( ) = ElementPreview { TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) } - -class InReplyToDetailsOtherProvider : InReplyToDetailsProvider() { - override val values: Sequence - get() = sequenceOf( - InReplyToDetails.Loading(eventId = EventId("\$anEventId")), - InReplyToDetails.Error(eventId = EventId("\$anEventId"), message = "An error message."), - ) -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt index e5e1daca8a..7b6bb471c7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt @@ -19,7 +19,6 @@ package io.element.android.features.messages.impl.timeline.components import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition @@ -27,28 +26,8 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -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.media.MediaSource -import io.element.android.libraries.matrix.api.poll.PollKind -import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.EventContent -import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent -import io.element.android.libraries.matrix.api.timeline.item.event.MessageType -import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.PollContent -import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails -import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentMapOf +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider @PreviewsDayNight @Composable @@ -93,100 +72,3 @@ internal fun TimelineItemEventRowWithReplyContentToPreview( } } } - -open class InReplyToDetailsProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aMessageContent( - body = "Message which are being replied.", - type = TextMessageType("Message which are being replied.", null) - ), - aMessageContent( - body = "Message which are being replied, and which was long enough to be displayed on two lines (only!).", - type = TextMessageType("Message which are being replied, and which was long enough to be displayed on two lines (only!).", null) - ), - aMessageContent( - body = "Video", - type = VideoMessageType("Video", null, null, MediaSource("url"), null), - ), - aMessageContent( - body = "Audio", - type = AudioMessageType("Audio", MediaSource("url"), null), - ), - aMessageContent( - body = "Voice", - type = VoiceMessageType("Voice", MediaSource("url"), null, null), - ), - aMessageContent( - body = "Image", - type = ImageMessageType("Image", null, null, MediaSource("url"), null), - ), - aMessageContent( - body = "Sticker", - type = StickerMessageType("Image", MediaSource("url"), null), - ), - aMessageContent( - body = "File", - type = FileMessageType("File", MediaSource("url"), null), - ), - aMessageContent( - body = "Location", - type = LocationMessageType("Location", "geo:1,2", null), - ), - aMessageContent( - body = "Notice", - type = NoticeMessageType("Notice", null), - ), - aMessageContent( - body = "Emote", - type = EmoteMessageType("Emote", null), - ), - PollContent( - question = "Poll which are being replied.", - kind = PollKind.Disclosed, - maxSelections = 1u, - answers = persistentListOf(), - votes = persistentMapOf(), - endTime = null, - isEdited = false, - ), - ).map { - aInReplyToDetails( - eventContent = it, - ) - } - - protected fun aMessageContent( - body: String, - type: MessageType, - ) = MessageContent( - body = body, - inReplyTo = null, - isEdited = false, - isThreaded = false, - type = type, - ) - - protected fun aInReplyToDetails( - eventContent: EventContent, - displayNameAmbiguous: Boolean = false, - ) = InReplyToDetails.Ready( - eventId = EventId("\$event"), - eventContent = eventContent, - senderId = UserId("@Sender:domain"), - senderProfile = aProfileTimelineDetailsReady( - displayNameAmbiguous = displayNameAmbiguous, - ), - textContent = (eventContent as? MessageContent)?.body.orEmpty(), - ) -} - -internal fun aProfileTimelineDetailsReady( - displayName: String? = "Sender", - displayNameAmbiguous: Boolean = false, - avatarUrl: String? = null, -) = ProfileTimelineDetails.Ready( - displayName = displayName, - displayNameAmbiguous = displayNameAmbiguous, - avatarUrl = avatarUrl, -) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index a725fcd2e3..7b23302dcd 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -66,6 +66,7 @@ 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.TransactionId import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MessageEventType @@ -84,6 +85,7 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender @@ -334,7 +336,7 @@ class MessagesPresenterTest { val finalState = awaitItem() assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.attachmentThumbnailInfo).isNotNull() + assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Ready::class.java) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -367,7 +369,7 @@ class MessagesPresenterTest { val finalState = awaitItem() assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.attachmentThumbnailInfo).isNotNull() + assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Ready::class.java) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -393,7 +395,7 @@ class MessagesPresenterTest { val finalState = awaitItem() assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.attachmentThumbnailInfo).isNotNull() + assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Ready::class.java) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -738,9 +740,8 @@ class MessagesPresenterTest { val finalState = awaitItem() assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.attachmentThumbnailInfo).isNotNull() - assertThat(replyMode.attachmentThumbnailInfo?.textContent) - .isEqualTo("What type of food should we have at the party?") + + assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Ready::class.java) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -761,6 +762,7 @@ class MessagesPresenterTest { analyticsService: FakeAnalyticsService = FakeAnalyticsService(), permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), endPollAction: EndPollAction = FakeEndPollAction(), + permalinkParser: PermalinkParser = FakePermalinkParser(), ): MessagesPresenter { val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) @@ -780,9 +782,9 @@ class MessagesPresenterTest { richTextEditorStateFactory = TestRichTextEditorStateFactory(), permissionsPresenterFactory = permissionsPresenterFactory, currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), - permalinkParser = FakePermalinkParser(), permalinkBuilder = FakePermalinkBuilder(), timelineController = TimelineController(matrixRoom), + permalinkParser = permalinkParser, draftService = FakeComposerDraftService(), ).apply { showTextFormatting = true @@ -835,7 +837,6 @@ class MessagesPresenterTest { readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), - messageSummaryFormatter = FakeMessageSummaryFormatter(), navigator = navigator, clipboardHelper = clipboardHelper, featureFlagsService = FakeFeatureFlagService(), @@ -843,6 +844,7 @@ class MessagesPresenterTest { dispatchers = coroutineDispatchers, htmlConverterProvider = FakeHtmlConverterProvider(), timelineController = TimelineController(matrixRoom), + permalinkParser = permalinkParser, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt index b6a605bdbf..7f2c3cfbeb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt @@ -18,8 +18,6 @@ package io.element.android.features.messages.impl.fixtures import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo import io.element.android.features.messages.impl.timeline.aTimelineItemReactions -import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady -import io.element.android.features.messages.impl.timeline.model.InReplyToDetails import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts @@ -35,6 +33,8 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails +import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady import kotlinx.collections.immutable.toImmutableList internal fun aMessageEvent( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index e7dbba99a2..d35356458e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -67,6 +67,7 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor @@ -1084,7 +1085,7 @@ fun anEditMode( transactionId: TransactionId? = null, ) = MessageComposerMode.Edit(eventId, message, transactionId) -fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, false, AN_EVENT_ID, A_MESSAGE) +fun aReplyMode() = MessageComposerMode.Reply(replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID)) fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE) private suspend fun TextEditorState.setHtml(html: String) { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt index 3cfc34ee51..ce53e05090 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt @@ -20,7 +20,6 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo import io.element.android.features.messages.impl.timeline.aTimelineItemReactions -import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts @@ -30,6 +29,7 @@ import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady import kotlinx.collections.immutable.toImmutableList import org.junit.Test diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index 303fb366b9..80d832e2de 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor @@ -712,7 +713,7 @@ class VoiceMessageComposerPresenterTest { ) } -private fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, false, AN_EVENT_ID, A_MESSAGE) +private fun aReplyMode() = MessageComposerMode.Reply(replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID)) private fun aVoiceMessageComposerEvent( isReply: Boolean = false diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index c19e4b7b93..b9c786210c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -582,9 +582,22 @@ class RustTimeline( } } - override suspend fun loadReplyDetails(eventId: EventId): Result { - return runCatching { - inner.loadReplyDetails(eventId.value).use(inReplyToMapper::map) + override suspend fun loadReplyDetails(eventId: EventId): Result = withContext(dispatcher) { + runCatching { + val timelineItem = _timelineItems.value.firstOrNull { timelineItem -> + timelineItem is MatrixTimelineItem.Event && timelineItem.eventId == eventId + } as? MatrixTimelineItem.Event + + if (timelineItem != null) { + InReplyTo.Ready( + eventId = eventId, + content = timelineItem.event.content, + senderId = timelineItem.event.sender, + senderProfile = timelineItem.event.senderProfile, + ) + } else { + inner.loadReplyDetails(eventId.value).use(inReplyToMapper::map) + } } } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt new file mode 100644 index 0000000000..72c3a8306c --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.ui.messages.reply + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +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.media.MediaSource +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent +import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageType +import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails +import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent +import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent +import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf + +open class InReplyToDetailsProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aMessageContent( + body = "Message which are being replied.", + type = TextMessageType("Message which are being replied.", null) + ), + aMessageContent( + body = "Message which are being replied, and which was long enough to be displayed on two lines (only!).", + type = TextMessageType("Message which are being replied, and which was long enough to be displayed on two lines (only!).", null) + ), + aMessageContent( + body = "Video", + type = VideoMessageType("Video", null, null, MediaSource("url"), null), + ), + aMessageContent( + body = "Audio", + type = AudioMessageType("Audio", MediaSource("url"), null), + ), + aMessageContent( + body = "Voice", + type = VoiceMessageType("Voice", MediaSource("url"), null, null), + ), + aMessageContent( + body = "Image", + type = ImageMessageType("Image", null, null, MediaSource("url"), null), + ), + aMessageContent( + body = "Sticker", + type = StickerMessageType("Image", MediaSource("url"), null), + ), + aMessageContent( + body = "File", + type = FileMessageType("File", MediaSource("url"), null), + ), + aMessageContent( + body = "Location", + type = LocationMessageType("Location", "geo:1,2", null), + ), + aMessageContent( + body = "Notice", + type = NoticeMessageType("Notice", null), + ), + aMessageContent( + body = "Emote", + type = EmoteMessageType("Emote", null), + ), + PollContent( + question = "Poll which are being replied.", + kind = PollKind.Disclosed, + maxSelections = 1u, + answers = persistentListOf(), + votes = persistentMapOf(), + endTime = null, + isEdited = false, + ), + ).map { + aInReplyToDetails( + eventContent = it, + ) + } +} + +class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() { + override val values: Sequence + get() = sequenceOf( + aMessageContent( + body = "Message which are being replied.", + type = TextMessageType("Message which are being replied.", null) + ), + ).map { + aInReplyToDetails( + displayNameAmbiguous = true, + eventContent = it, + ) + } +} + +class InReplyToDetailsInformativeProvider : InReplyToDetailsProvider() { + override val values: Sequence + get() = sequenceOf( + RedactedContent, + UnableToDecryptContent(UnableToDecryptContent.Data.Unknown), + ).map { + aInReplyToDetails( + eventContent = it, + ) + } +} + +class InReplyToDetailsOtherProvider : InReplyToDetailsProvider() { + override val values: Sequence + get() = sequenceOf( + InReplyToDetails.Loading(eventId = EventId("\$anEventId")), + InReplyToDetails.Error(eventId = EventId("\$anEventId"), message = "An error message."), + ) +} + + +private fun aMessageContent( + body: String, + type: MessageType, +) = MessageContent( + body = body, + inReplyTo = null, + isEdited = false, + isThreaded = false, + type = type, +) + +private fun aInReplyToDetails( + eventContent: EventContent, + displayNameAmbiguous: Boolean = false, +) = InReplyToDetails.Ready( + eventId = EventId("\$event"), + eventContent = eventContent, + senderId = UserId("@Sender:domain"), + senderProfile = aProfileTimelineDetailsReady( + displayNameAmbiguous = displayNameAmbiguous, + ), + textContent = (eventContent as? MessageContent)?.body.orEmpty(), +) + +fun aProfileTimelineDetailsReady( + displayName: String? = "Sender", + displayNameAmbiguous: Boolean = false, + avatarUrl: String? = null, +) = ProfileTimelineDetails.Ready( + displayName = displayName, + displayNameAmbiguous = displayNameAmbiguous, + avatarUrl = avatarUrl, +) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt index c50c81fb87..644125e859 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt @@ -46,6 +46,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToBox +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.ui.strings.CommonStrings @@ -61,9 +63,7 @@ internal fun ComposerModeView( is MessageComposerMode.Reply -> { ReplyToModeView( modifier = Modifier.padding(8.dp), - senderName = composerMode.senderName, - text = composerMode.defaultContent, - attachmentThumbnailInfo = composerMode.attachmentThumbnailInfo, + replyToDetails = composerMode.replyToDetails, onResetComposerMode = onResetComposerMode, ) } @@ -118,9 +118,7 @@ private fun EditingModeView( @Composable private fun ReplyToModeView( - senderName: String, - text: String?, - attachmentThumbnailInfo: AttachmentThumbnailInfo?, + replyToDetails: InReplyToDetails, onResetComposerMode: () -> Unit, modifier: Modifier = Modifier, ) { @@ -130,42 +128,7 @@ private fun ReplyToModeView( .background(MaterialTheme.colorScheme.surface) .padding(4.dp) ) { - if (attachmentThumbnailInfo != null) { - AttachmentThumbnail( - info = attachmentThumbnailInfo, - backgroundColor = MaterialTheme.colorScheme.surfaceVariant, - modifier = Modifier - .size(36.dp) - .clip(RoundedCornerShape(9.dp)) - ) - } - Spacer(modifier = Modifier.width(8.dp)) - Column( - modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - ) { - Text( - text = senderName, - modifier = Modifier - .fillMaxWidth() - .clipToBounds(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ElementTheme.typography.fontBodySmMedium, - textAlign = TextAlign.Start, - color = ElementTheme.materialColors.primary, - ) - Text( - modifier = Modifier.fillMaxWidth(), - text = text.orEmpty(), - style = ElementTheme.typography.fontBodyMdRegular, - textAlign = TextAlign.Start, - color = ElementTheme.materialColors.secondary, - maxLines = if (attachmentThumbnailInfo != null) 1 else 2, - overflow = TextOverflow.Ellipsis, - ) - } + InReplyToBox(inReplyTo = replyToDetails, modifier = Modifier.weight(1f)) Icon( imageVector = CompoundIcons.Close(), contentDescription = stringResource(CommonStrings.action_close), diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 81a019a43a..b158359814 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.components.media.createFakeWaveform @@ -52,12 +53,10 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser -import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH -import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo -import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.textcomposer.components.ComposerOptionsButton @@ -133,8 +132,8 @@ fun TextComposer( } val layoutModifier = modifier - .fillMaxSize() - .height(IntrinsicSize.Min) + .fillMaxSize() + .height(IntrinsicSize.Min) val composerOptionsButton: @Composable () -> Unit = remember { @Composable { @@ -335,8 +334,8 @@ private fun StandardLayout( if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) { Box( modifier = Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) - .size(48.dp), + .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) + .size(48.dp), contentAlignment = Alignment.Center, ) { voiceDeleteButton() @@ -346,8 +345,8 @@ private fun StandardLayout( } Box( modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) ) { voiceRecording() } @@ -360,16 +359,16 @@ private fun StandardLayout( } Box( modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) ) { textInput() } } Box( - Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) - .size(48.dp), + Modifier + .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) + .size(48.dp), contentAlignment = Alignment.Center, ) { endButton() @@ -391,8 +390,8 @@ private fun TextFormattingLayout( ) { Box( modifier = Modifier - .weight(1f) - .padding(horizontal = 12.dp) + .weight(1f) + .padding(horizontal = 12.dp) ) { textInput() } @@ -436,11 +435,11 @@ private fun TextInputBox( Column( modifier = Modifier - .clip(roundedCorners) - .border(0.5.dp, borderColor, roundedCorners) - .background(color = bgColor) - .requiredHeightIn(min = 42.dp) - .fillMaxSize(), + .clip(roundedCorners) + .border(0.5.dp, borderColor, roundedCorners) + .background(color = bgColor) + .requiredHeightIn(min = 42.dp) + .fillMaxSize(), ) { if (composerMode is MessageComposerMode.Special) { ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) @@ -448,9 +447,9 @@ private fun TextInputBox( val defaultTypography = ElementTheme.typography.fontBodyLgRegular Box( modifier = Modifier - .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 42.dp) - // Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail - .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), + .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 42.dp) + // Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail + .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), contentAlignment = Alignment.CenterStart, ) { // Placeholder @@ -496,8 +495,8 @@ private fun TextInput( // This prevents it gaining focus and mutating the state. registerStateUpdates = !subcomposing, modifier = Modifier - .padding(top = 6.dp, bottom = 6.dp) - .fillMaxWidth(), + .padding(top = 6.dp, bottom = 6.dp) + .fillMaxWidth(), style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus), resolveMentionDisplay = resolveMentionDisplay, resolveRoomMentionDisplay = resolveRoomMentionDisplay, @@ -625,124 +624,15 @@ internal fun MarkdownTextComposerEditPreview() = ElementPreview { @PreviewsDayNight @Composable -internal fun TextComposerReplyPreview() = ElementPreview { - PreviewColumn( - items = persistentListOf( - { - ATextComposer( - TextEditorState.Rich(aRichTextEditorState()), - voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Reply( - isThreaded = false, - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = null, - defaultContent = "A message\n" + - "With several lines\n" + - "To preview larger textfields and long lines with overflow" - ), - enableVoiceMessages = true, - currentUserId = UserId("@alice:localhost") - ) - }, - { - ATextComposer( - TextEditorState.Rich(aRichTextEditorState()), - voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Reply( - isThreaded = true, - senderName = "Alice with a very long name to test overflow in the composer", - eventId = EventId("$1234"), - attachmentThumbnailInfo = null, - defaultContent = "A message\n" + - "With several lines\n" + - "To preview larger textfields and long lines with overflow" - ), - enableVoiceMessages = true, - currentUserId = UserId("@alice:localhost") - ) - }, - { - ATextComposer( - TextEditorState.Rich(aRichTextEditorState(initialText = "A message")), - voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Reply( - isThreaded = true, - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = AttachmentThumbnailInfo( - thumbnailSource = MediaSource("https://domain.com/image.jpg"), - textContent = "image.jpg", - type = AttachmentThumbnailType.Image, - blurHash = A_BLUR_HASH, - ), - defaultContent = "image.jpg" - ), - enableVoiceMessages = true, - currentUserId = UserId("@alice:localhost") - ) - }, - { - ATextComposer( - TextEditorState.Rich(aRichTextEditorState(initialText = "A message")), - voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Reply( - isThreaded = false, - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = AttachmentThumbnailInfo( - thumbnailSource = MediaSource("https://domain.com/video.mp4"), - textContent = "video.mp4", - type = AttachmentThumbnailType.Video, - blurHash = A_BLUR_HASH, - ), - defaultContent = "video.mp4" - ), - enableVoiceMessages = true, - currentUserId = UserId("@alice:localhost") - ) - }, - { - ATextComposer( - TextEditorState.Rich(aRichTextEditorState(initialText = "A message")), - voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Reply( - isThreaded = false, - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = AttachmentThumbnailInfo( - thumbnailSource = null, - textContent = "logs.txt", - type = AttachmentThumbnailType.File, - blurHash = null, - ), - defaultContent = "logs.txt" - ), - enableVoiceMessages = true, - currentUserId = UserId("@alice:localhost") - ) - }, - { - ATextComposer( - TextEditorState.Rich(aRichTextEditorState(initialText = "A message", initialFocus = true)), - voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Reply( - isThreaded = false, - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = AttachmentThumbnailInfo( - thumbnailSource = null, - textContent = null, - type = AttachmentThumbnailType.Location, - blurHash = null, - ), - defaultContent = "Shared location" - ), - enableVoiceMessages = true, - currentUserId = UserId("@alice:localhost") - ) - } - ) +internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview { + ATextComposer( + TextEditorState.Rich(aRichTextEditorState()), + voiceMessageState = VoiceMessageState.Idle, + composerMode = MessageComposerMode.Reply( + replyToDetails = inReplyToDetails, + ), + enableVoiceMessages = true, + currentUserId = UserId("@alice:localhost") ) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index 2e2869aec7..eae5ec8678 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -16,37 +16,28 @@ package io.element.android.libraries.textcomposer.model -import android.os.Parcelable import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId -import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo -import kotlinx.parcelize.Parcelize +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails +import io.element.android.libraries.matrix.ui.messages.reply.eventId @Immutable -sealed interface MessageComposerMode : Parcelable { - @Parcelize +sealed interface MessageComposerMode { data object Normal : MessageComposerMode - sealed class Special(open val eventId: EventId?, open val defaultContent: String) : - MessageComposerMode + sealed interface Special : MessageComposerMode - @Parcelize - data class Edit(override val eventId: EventId?, override val defaultContent: String, val transactionId: TransactionId?) : - Special(eventId, defaultContent) + data class Edit(val eventId: EventId?, val content: String, val transactionId: TransactionId?) : Special - @Parcelize - class Quote(override val eventId: EventId, override val defaultContent: String) : - Special(eventId, defaultContent) + class Quote(val eventId: EventId, val content: String) : Special - @Parcelize class Reply( - val senderName: String, - val attachmentThumbnailInfo: AttachmentThumbnailInfo?, - val isThreaded: Boolean, - override val eventId: EventId, - override val defaultContent: String - ) : Special(eventId, defaultContent) + val replyToDetails: InReplyToDetails + ) : Special { + val eventId: EventId = replyToDetails.eventId() + } val relatedEventId: EventId? get() = when (this) { @@ -63,5 +54,8 @@ sealed interface MessageComposerMode : Parcelable { get() = this is Reply val inThread: Boolean - get() = this is Reply && isThreaded + get() = this is Reply && + replyToDetails is InReplyToDetails.Ready && + replyToDetails.eventContent is MessageContent && + (replyToDetails.eventContent as MessageContent).isThreaded }