Browse Source

Reply : refactor so we can use ReplyToDetails in Composer and Timeline

pull/3099/head
ganfra 3 months ago
parent
commit
5597a1743a
  1. 4
      features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt
  2. 91
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
  3. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  4. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt
  5. 17
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowDisambiguatedPreview.kt
  6. 15
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt
  7. 10
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt
  8. 120
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt
  9. 18
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
  10. 4
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt
  11. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt
  12. 2
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt
  13. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
  14. 19
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt
  15. 175
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt
  16. 47
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt
  17. 180
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
  18. 36
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt

4
features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt

@ -379,7 +379,7 @@ class SendLocationPresenterTest { @@ -379,7 +379,7 @@ class SendLocationPresenterTest {
fakeMessageComposerContext.apply {
composerMode = MessageComposerMode.Edit(
eventId = null,
defaultContent = "",
content = "",
transactionId = null
)
}
@ -427,7 +427,7 @@ class SendLocationPresenterTest { @@ -427,7 +427,7 @@ class SendLocationPresenterTest {
fakeMessageComposerContext.apply {
composerMode = MessageComposerMode.Edit(
eventId = null,
defaultContent = "",
content = "",
transactionId = null
)
}

91
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 @@ -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 @@ -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 @@ -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( @@ -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<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
@AssistedFactory
@ -340,66 +331,20 @@ class MessagesPresenter @AssistedInject constructor( @@ -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) {

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

@ -184,9 +184,9 @@ class MessageComposerPresenter @Inject constructor( @@ -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
}

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt

@ -16,7 +16,6 @@ @@ -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 @@ -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

17
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 @@ -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( @@ -33,18 +33,3 @@ internal fun TimelineItemEventRowDisambiguatedPreview(
displayNameAmbiguous = true,
)
}
class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() {
override val values: Sequence<InReplyToDetails>
get() = sequenceOf(
aMessageContent(
body = "Message which are being replied.",
type = TextMessageType("Message which are being replied.", null)
),
).map {
aInReplyToDetails(
displayNameAmbiguous = true,
eventContent = it,
)
}
}

15
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 @@ -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( @@ -31,15 +30,3 @@ internal fun TimelineItemEventRowWithReplyInformativePreview(
) = ElementPreview {
TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails)
}
class InReplyToDetailsInformativeProvider : InReplyToDetailsProvider() {
override val values: Sequence<InReplyToDetails>
get() = sequenceOf(
RedactedContent,
UnableToDecryptContent(UnableToDecryptContent.Data.Unknown),
).map {
aInReplyToDetails(
eventContent = it,
)
}
}

10
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 @@ -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( @@ -30,11 +30,3 @@ internal fun TimelineItemEventRowWithReplyOtherPreview(
) = ElementPreview {
TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails)
}
class InReplyToDetailsOtherProvider : InReplyToDetailsProvider() {
override val values: Sequence<InReplyToDetails>
get() = sequenceOf(
InReplyToDetails.Loading(eventId = EventId("\$anEventId")),
InReplyToDetails.Error(eventId = EventId("\$anEventId"), message = "An error message."),
)
}

120
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 @@ -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 @@ -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( @@ -93,100 +72,3 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(
}
}
}
open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> {
override val values: Sequence<InReplyToDetails>
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,
)

18
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 @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -843,6 +844,7 @@ class MessagesPresenterTest {
dispatchers = coroutineDispatchers,
htmlConverterProvider = FakeHtmlConverterProvider(),
timelineController = TimelineController(matrixRoom),
permalinkParser = permalinkParser,
)
}
}

4
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 @@ -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 @@ -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(

3
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 @@ -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( @@ -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) {

2
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 @@ -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 @@ -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

3
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 @@ -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 { @@ -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

19
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt

@ -582,9 +582,22 @@ class RustTimeline( @@ -582,9 +582,22 @@ class RustTimeline(
}
}
override suspend fun loadReplyDetails(eventId: EventId): Result<InReplyTo> {
return runCatching {
inner.loadReplyDetails(eventId.value).use(inReplyToMapper::map)
override suspend fun loadReplyDetails(eventId: EventId): Result<InReplyTo> = 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)
}
}
}
}

175
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt

@ -0,0 +1,175 @@ @@ -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<InReplyToDetails> {
override val values: Sequence<InReplyToDetails>
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<InReplyToDetails>
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<InReplyToDetails>
get() = sequenceOf(
RedactedContent,
UnableToDecryptContent(UnableToDecryptContent.Data.Unknown),
).map {
aInReplyToDetails(
eventContent = it,
)
}
}
class InReplyToDetailsOtherProvider : InReplyToDetailsProvider() {
override val values: Sequence<InReplyToDetails>
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,
)

47
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 @@ -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( @@ -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( @@ -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( @@ -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),

180
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt

@ -42,6 +42,7 @@ import androidx.compose.ui.Modifier @@ -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 @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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 { @@ -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")
)
}

36
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt

@ -16,37 +16,28 @@ @@ -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 { @@ -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
}

Loading…
Cancel
Save