Use RTE TextView for timeline text messages, add mention pills to messages (#1990)
* Add `formattedBody` to `TimelineItemTextBasedContent`. This is pre-computed when timeline events are being mapped from the Rust SDK. * Update `HtmlConverterProvider` styles. * Improve `MentionSpan` to add missing `@` or `#` if needed * Replace `HtmlDocument` with the `TextView` based component * Improve extra padding calculation for timestamp by rounding the float offset result instead of truncating it. * Remove composer line height workaround * Use `ElementRichTextEditorStyle` instead of `RichTextEditorDefaults` for the theming * Use slightly different styles for composer and messages (top/bottom line height discrepancies, mostly). * Add `formattedBody` to notice and emote events. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
e4a07dfa38
commit
1e86d8279b
232 changed files with 754 additions and 1124 deletions
|
|
@ -16,32 +16,52 @@
|
|||
|
||||
package io.element.android.libraries.textcomposer
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.libraries.designsystem.theme.bgSubtleTertiary
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.bgSubtleTertiary
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorDefaults
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorStyle
|
||||
|
||||
internal object ElementRichTextEditorStyle {
|
||||
object ElementRichTextEditorStyle {
|
||||
@Composable
|
||||
fun create(
|
||||
fun composerStyle(
|
||||
hasFocus: Boolean,
|
||||
) : RichTextEditorStyle {
|
||||
val baseStyle = common()
|
||||
return baseStyle.copy(
|
||||
text = baseStyle.text.copy(
|
||||
color = if (hasFocus) {
|
||||
ElementTheme.materialColors.primary
|
||||
} else {
|
||||
ElementTheme.materialColors.secondary
|
||||
},
|
||||
lineHeight = TextUnit.Unspecified,
|
||||
includeFontPadding = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun textStyle(): RichTextEditorStyle {
|
||||
return common()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun common(): RichTextEditorStyle {
|
||||
val colors = ElementTheme.colors
|
||||
val m3colors = MaterialTheme.colorScheme
|
||||
val codeCornerRadius = 4.dp
|
||||
val codeBorderWidth = 1.dp
|
||||
return RichTextEditorDefaults.style(
|
||||
text = RichTextEditorDefaults.textStyle(
|
||||
color = if (hasFocus) {
|
||||
m3colors.primary
|
||||
} else {
|
||||
m3colors.secondary
|
||||
},
|
||||
lineHeight = 16.25.sp,
|
||||
color = LocalTextStyle.current.color.takeIf { it.isSpecified } ?: LocalContentColor.current,
|
||||
fontStyle = LocalTextStyle.current.fontStyle,
|
||||
lineHeight = LocalTextStyle.current.lineHeight,
|
||||
includeFontPadding = false,
|
||||
),
|
||||
cursor = RichTextEditorDefaults.cursorStyle(
|
||||
color = colors.iconAccentTertiary,
|
||||
|
|
|
|||
|
|
@ -441,9 +441,7 @@ private fun TextInput(
|
|||
modifier = Modifier
|
||||
.padding(top = 6.dp, bottom = 6.dp)
|
||||
.fillMaxWidth(),
|
||||
style = ElementRichTextEditorStyle.create(
|
||||
hasFocus = state.hasFocus
|
||||
),
|
||||
style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus),
|
||||
resolveMentionDisplay = resolveMentionDisplay,
|
||||
resolveRoomMentionDisplay = resolveRoomMentionDisplay,
|
||||
onError = onError
|
||||
|
|
|
|||
|
|
@ -23,20 +23,59 @@ import android.text.style.ReplacementSpan
|
|||
import kotlin.math.roundToInt
|
||||
|
||||
class MentionSpan(
|
||||
val type: Type,
|
||||
val backgroundColor: Int,
|
||||
val textColor: Int,
|
||||
) : ReplacementSpan() {
|
||||
|
||||
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
|
||||
return paint.measureText(text, start, end).roundToInt() + 40
|
||||
val mentionText = getActualText(text, start)
|
||||
var actualEnd = end
|
||||
if (mentionText != text.toString()) {
|
||||
actualEnd = end + 1
|
||||
}
|
||||
return paint.measureText(mentionText, start, actualEnd).roundToInt() + 40
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
|
||||
val textSize = paint.measureText(text, start, end)
|
||||
val rect = RectF(x, top.toFloat(), x + textSize + 40, bottom.toFloat())
|
||||
val mentionText = getActualText(text, start)
|
||||
var actualEnd = end
|
||||
if (mentionText != text.toString()) {
|
||||
actualEnd = end + 1
|
||||
}
|
||||
val textWidth = paint.measureText(mentionText, start, actualEnd)
|
||||
// Extra vertical space to add below the baseline (y). This helps us center the span vertically
|
||||
val extraVerticalSpace = y + paint.ascent() + paint.descent() - top
|
||||
val rect = RectF(x, top.toFloat(), x + textWidth + 40, y.toFloat() + extraVerticalSpace)
|
||||
paint.color = backgroundColor
|
||||
canvas.drawRoundRect(rect, rect.height() / 2, rect.height() / 2, paint)
|
||||
paint.color = textColor
|
||||
canvas.drawText(text!!, start, end, x + 20, y.toFloat(), paint)
|
||||
canvas.drawText(mentionText, start, actualEnd, x + 20, y.toFloat(), paint)
|
||||
}
|
||||
|
||||
private fun getActualText(text: CharSequence?, start: Int): String {
|
||||
return when (type) {
|
||||
Type.USER -> {
|
||||
val mentionText = text.toString()
|
||||
if (start in mentionText.indices && mentionText[start] != '@') {
|
||||
mentionText.replaceRange(start, start, "@")
|
||||
} else {
|
||||
mentionText
|
||||
}
|
||||
}
|
||||
Type.ROOM -> {
|
||||
val mentionText = text.toString()
|
||||
if (start in mentionText.indices && mentionText[start] != '#') {
|
||||
mentionText.replaceRange(start, start, "#")
|
||||
} else {
|
||||
mentionText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
USER,
|
||||
ROOM,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,18 +60,21 @@ class MentionSpanProvider(
|
|||
permalinkData is PermalinkData.UserLink -> {
|
||||
val isCurrentUser = permalinkData.userId == currentSessionId.value
|
||||
MentionSpan(
|
||||
type = MentionSpan.Type.USER,
|
||||
backgroundColor = if (isCurrentUser) currentUserBackgroundColor else otherBackgroundColor,
|
||||
textColor = if (isCurrentUser) currentUserTextColor else otherTextColor,
|
||||
)
|
||||
}
|
||||
text == "@room" && permalinkData is PermalinkData.FallbackLink -> {
|
||||
MentionSpan(
|
||||
type = MentionSpan.Type.USER,
|
||||
backgroundColor = otherBackgroundColor,
|
||||
textColor = otherTextColor,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
MentionSpan(
|
||||
type = MentionSpan.Type.ROOM,
|
||||
backgroundColor = otherBackgroundColor,
|
||||
textColor = otherTextColor,
|
||||
)
|
||||
|
|
@ -97,17 +100,26 @@ internal fun MentionSpanPreview() {
|
|||
provider.setup()
|
||||
|
||||
val textColor = ElementTheme.colors.textPrimary.toArgb()
|
||||
val mentionSpan = provider.getMentionSpanFor("me", "https://matrix.to/#/@me:matrix.org")
|
||||
val mentionSpan2 = provider.getMentionSpanFor("other", "https://matrix.to/#/@other:matrix.org")
|
||||
fun mentionSpanMe() = provider.getMentionSpanFor("me", "https://matrix.to/#/@me:matrix.org")
|
||||
fun mentionSpanOther() = provider.getMentionSpanFor("other", "https://matrix.to/#/@other:matrix.org")
|
||||
fun mentionSpanRoom() = provider.getMentionSpanFor("room", "https://matrix.to/#/#room:matrix.org")
|
||||
AndroidView(factory = { context ->
|
||||
TextView(context).apply {
|
||||
includeFontPadding = false
|
||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
text = buildSpannedString {
|
||||
append("This is a ")
|
||||
append("@mention", mentionSpan, 0)
|
||||
append("@mention", mentionSpanMe(), 0)
|
||||
append(" to the current user and this is a ")
|
||||
append("@mention", mentionSpan2, 0)
|
||||
append(" to other user")
|
||||
append("@mention", mentionSpanOther(), 0)
|
||||
append(" to other user. This one is for a room: ")
|
||||
append("#room:matrix.org", mentionSpanRoom(), 0)
|
||||
append("\n\n")
|
||||
append("This ")
|
||||
append("mention", mentionSpanMe(), 0)
|
||||
append(" didn't have an '@' and it was automatically added, same as this ")
|
||||
append("room:matrix.org", mentionSpanRoom(), 0)
|
||||
append(" one, which had no leading '#'.")
|
||||
}
|
||||
setTextColor(textColor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,14 +46,21 @@ class MentionSpanProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getting mention span for current user should return a MentionSpan with custom colors`() {
|
||||
val mentionSpan = mentionSpanProvider.getMentionSpanFor("me", "https://matrix.to/#/${currentUserId.value}")
|
||||
val mentionSpan = mentionSpanProvider.getMentionSpanFor("@me:matrix.org", "https://matrix.to/#/${currentUserId.value}")
|
||||
assertThat(mentionSpan.backgroundColor).isEqualTo(myUserColor)
|
||||
assertThat(mentionSpan.textColor).isEqualTo(myUserColor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getting mention span for other user should return a MentionSpan with normal colors`() {
|
||||
val mentionSpan = mentionSpanProvider.getMentionSpanFor("other", "https://matrix.to/#/@other:matrix.org")
|
||||
val mentionSpan = mentionSpanProvider.getMentionSpanFor("@other:matrix.org", "https://matrix.to/#/@other:matrix.org")
|
||||
assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor)
|
||||
assertThat(mentionSpan.textColor).isEqualTo(otherColor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getting mention span for a room should return a MentionSpan with normal colors`() {
|
||||
val mentionSpan = mentionSpanProvider.getMentionSpanFor("#room:matrix.org", "https://matrix.to/#/#room:matrix.org")
|
||||
assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor)
|
||||
assertThat(mentionSpan.textColor).isEqualTo(otherColor)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue