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

This commit is contained in:
ganfra 2024-06-21 12:23:17 +02:00
parent 78da033541
commit 71667dc235
18 changed files with 290 additions and 460 deletions

View file

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

View file

@ -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")
)
}

View file

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