Reply : refactor so we can use ReplyToDetails in Composer and Timeline
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<MessagesState> {
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<InReplyToDetails>
|
||||
get() = sequenceOf(
|
||||
aMessageContent(
|
||||
body = "Message which are being replied.",
|
||||
type = TextMessageType("Message which are being replied.", null)
|
||||
),
|
||||
).map {
|
||||
aInReplyToDetails(
|
||||
displayNameAmbiguous = true,
|
||||
eventContent = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<InReplyToDetails>
|
||||
get() = sequenceOf(
|
||||
RedactedContent,
|
||||
UnableToDecryptContent(UnableToDecryptContent.Data.Unknown),
|
||||
).map {
|
||||
aInReplyToDetails(
|
||||
eventContent = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<InReplyToDetails>
|
||||
get() = sequenceOf(
|
||||
InReplyToDetails.Loading(eventId = EventId("\$anEventId")),
|
||||
InReplyToDetails.Error(eventId = EventId("\$anEventId"), message = "An error message."),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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),
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user