Merge branch 'develop' into feature/fga/draft_support

This commit is contained in:
ganfra 2024-06-26 14:39:44 +02:00
commit 1b56d1b97a
485 changed files with 2939 additions and 1591 deletions

View file

@ -52,9 +52,9 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
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.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache
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
@ -70,7 +70,7 @@ import io.element.android.libraries.textcomposer.components.VoiceMessageRecordin
import io.element.android.libraries.textcomposer.components.markdown.MarkdownTextInput
import io.element.android.libraries.textcomposer.components.markdown.aMarkdownTextEditorState
import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape
import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanProvider
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.Suggestion
import io.element.android.libraries.textcomposer.model.TextEditorState
@ -93,7 +93,6 @@ fun TextComposer(
permalinkParser: PermalinkParser,
composerMode: MessageComposerMode,
enableVoiceMessages: Boolean,
currentUserId: UserId,
onRequestFocus: () -> Unit,
onSendMessage: () -> Unit,
onResetComposerMode: () -> Unit,
@ -145,6 +144,8 @@ fun TextComposer(
}
}
val userProfileCache = LocalRoomMemberProfilesCache.current
val placeholder = if (composerMode.inThread) {
stringResource(id = CommonStrings.action_reply_in_thread)
} else {
@ -154,17 +155,22 @@ fun TextComposer(
is TextEditorState.Rich -> {
remember(state.richTextEditorState, subcomposing, composerMode, onResetComposerMode, onError) {
@Composable {
val mentionSpanProvider = rememberMentionSpanProvider(
currentUserId = currentUserId,
permalinkParser = permalinkParser,
)
val mentionSpanProvider = LocalMentionSpanProvider.current
TextInput(
state = state.richTextEditorState,
subcomposing = subcomposing,
placeholder = placeholder,
composerMode = composerMode,
onResetComposerMode = onResetComposerMode,
resolveMentionDisplay = { text, url -> TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor(text, url)) },
resolveMentionDisplay = { text, url ->
val permalinkData = permalinkParser.parse(url)
if (permalinkData is PermalinkData.UserLink) {
val displayNameOrId = userProfileCache.getDisplayName(permalinkData.userId) ?: permalinkData.userId.value
TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor(displayNameOrId, url))
} else {
TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor(text, url))
}
},
resolveRoomMentionDisplay = { TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor("@room", "#")) },
onError = onError,
onTyping = onTyping,
@ -518,7 +524,6 @@ internal fun TextComposerSimplePreview() = ElementPreview {
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost"),
)
},
{
@ -527,7 +532,6 @@ internal fun TextComposerSimplePreview() = ElementPreview {
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
},
{
@ -541,7 +545,6 @@ internal fun TextComposerSimplePreview() = ElementPreview {
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
},
{
@ -550,7 +553,6 @@ internal fun TextComposerSimplePreview() = ElementPreview {
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
}
)
@ -567,7 +569,6 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
showTextFormatting = true,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
}, {
ATextComposer(
@ -576,7 +577,6 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
showTextFormatting = true,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
}, {
ATextComposer(
@ -589,7 +589,6 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
showTextFormatting = true,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
}))
}
@ -603,7 +602,6 @@ internal fun TextComposerEditPreview() = ElementPreview {
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Edit(EventId("$1234"), TransactionId("1234"), "Some text"),
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
}))
}
@ -617,7 +615,6 @@ internal fun MarkdownTextComposerEditPreview() = ElementPreview {
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Edit(EventId("$1234"), TransactionId("1234"), "Some text"),
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
}))
}
@ -626,13 +623,12 @@ internal fun MarkdownTextComposerEditPreview() = ElementPreview {
@Composable
internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview {
ATextComposer(
TextEditorState.Rich(aRichTextEditorState()),
state = TextEditorState.Rich(aRichTextEditorState()),
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Reply(
replyToDetails = inReplyToDetails,
),
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
}
@ -647,7 +643,6 @@ internal fun TextComposerVoicePreview() = ElementPreview {
voiceMessageState = voiceMessageState,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
currentUserId = UserId("@alice:localhost")
)
PreviewColumn(items = persistentListOf({
VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, createFakeWaveform()))
@ -708,7 +703,6 @@ private fun ATextComposer(
voiceMessageState: VoiceMessageState,
composerMode: MessageComposerMode,
enableVoiceMessages: Boolean,
currentUserId: UserId,
showTextFormatting: Boolean = false,
) {
TextComposer(
@ -720,7 +714,6 @@ private fun ATextComposer(
},
composerMode = composerMode,
enableVoiceMessages = enableVoiceMessages,
currentUserId = currentUserId,
onRequestFocus = {},
onSendMessage = {},
onResetComposerMode = {},

View file

@ -21,12 +21,14 @@ import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Typeface
import android.text.style.ReplacementSpan
import androidx.core.text.getSpans
import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.wysiwyg.view.spans.CustomMentionSpan
import kotlin.math.min
import kotlin.math.roundToInt
class MentionSpan(
val text: String,
text: String,
val rawValue: String,
val type: Type,
val backgroundColor: Int,
@ -39,23 +41,27 @@ class MentionSpan(
private const val MAX_LENGTH = 20
}
private var actualText: CharSequence? = null
private var textWidth = 0
private val backgroundPaint = Paint().apply {
isAntiAlias = true
color = backgroundColor
}
var text: String = text
set(value) {
field = value
mentionText = getActualText(text)
}
private var mentionText: CharSequence = getActualText(text)
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
val mentionText = getActualText(this.text)
paint.typeface = typeface
textWidth = paint.measureText(mentionText, 0, mentionText.length).roundToInt()
return textWidth + startPadding + endPadding
}
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
val mentionText = getActualText(this.text)
// Extra vertical space to add below the baseline (y). This helps us center the span vertically
val extraVerticalSpace = y + paint.ascent() + paint.descent() - top
@ -68,7 +74,6 @@ class MentionSpan(
}
private fun getActualText(text: String): CharSequence {
if (actualText != null) return actualText!!
return buildString {
val mentionText = text.orEmpty()
when (type) {
@ -87,7 +92,6 @@ class MentionSpan(
if (mentionText.length > MAX_LENGTH) {
append("")
}
actualText = this
}
}
@ -96,3 +100,18 @@ class MentionSpan(
ROOM,
}
}
fun CharSequence.getMentionSpans(): List<MentionSpan> {
return if (this is android.text.Spanned) {
val customMentionSpans = getSpans<CustomMentionSpan>()
if (customMentionSpans.isNotEmpty()) {
// If we have custom mention spans created by the RTE, we need to extract the provided spans and filter them
customMentionSpans.map { it.providedSpan }.filterIsInstance<MentionSpan>()
} else {
// Otherwise try to get the spans directly
getSpans<MentionSpan>().toList()
}
} else {
emptyList()
}
}

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.textcomposer.mentions
import android.graphics.Color
import android.graphics.Typeface
import android.net.Uri
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.foundation.layout.PaddingValues
@ -25,12 +26,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.buildSpannedString
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -40,7 +45,6 @@ import io.element.android.libraries.designsystem.theme.currentUserMentionPillTex
import io.element.android.libraries.designsystem.theme.mentionPillBackground
import io.element.android.libraries.designsystem.theme.mentionPillText
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
@ -48,22 +52,28 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import kotlinx.collections.immutable.persistentListOf
@Stable
class MentionSpanProvider(
private val currentSessionId: SessionId,
class MentionSpanProvider @AssistedInject constructor(
@Assisted private val currentSessionId: String,
private val permalinkParser: PermalinkParser,
private var currentUserTextColor: Int = 0,
private var currentUserBackgroundColor: Int = Color.WHITE,
private var otherTextColor: Int = 0,
private var otherBackgroundColor: Int = Color.WHITE,
) {
@AssistedFactory
interface Factory {
fun create(currentSessionId: String): MentionSpanProvider
}
private val paddingValues = PaddingValues(start = 4.dp, end = 6.dp)
private val paddingValuesPx = mutableStateOf(0 to 0)
private val typeface = mutableStateOf(Typeface.DEFAULT)
internal var currentUserTextColor: Int = 0
internal var currentUserBackgroundColor: Int = Color.WHITE
internal var otherTextColor: Int = 0
internal var otherBackgroundColor: Int = Color.WHITE
@Suppress("ComposableNaming")
@Composable
internal fun setup() {
fun updateStyles() {
currentUserTextColor = ElementTheme.colors.currentUserMentionPillText.toArgb()
currentUserBackgroundColor = ElementTheme.colors.currentUserMentionPillBackground.toArgb()
otherTextColor = ElementTheme.colors.mentionPillText.toArgb()
@ -82,7 +92,7 @@ class MentionSpanProvider(
val (startPaddingPx, endPaddingPx) = paddingValuesPx.value
return when {
permalinkData is PermalinkData.UserLink -> {
val isCurrentUser = permalinkData.userId == currentSessionId
val isCurrentUser = permalinkData.userId.value == currentSessionId
MentionSpan(
text = text,
rawValue = permalinkData.userId.toString(),
@ -134,43 +144,30 @@ class MentionSpanProvider(
}
}
@Composable
fun rememberMentionSpanProvider(
currentUserId: SessionId,
permalinkParser: PermalinkParser,
): MentionSpanProvider {
val provider = remember(currentUserId) {
MentionSpanProvider(
currentSessionId = currentUserId,
permalinkParser = permalinkParser,
)
}
provider.setup()
return provider
}
@PreviewsDayNight
@Composable
internal fun MentionSpanPreview() {
val provider = rememberMentionSpanProvider(
currentUserId = SessionId("@me:matrix.org"),
permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return when (uriString) {
"https://matrix.to/#/@me:matrix.org" -> PermalinkData.UserLink(UserId("@me:matrix.org"))
"https://matrix.to/#/@other:matrix.org" -> PermalinkData.UserLink(UserId("@other:matrix.org"))
"https://matrix.to/#/#room:matrix.org" -> PermalinkData.RoomLink(
roomIdOrAlias = RoomAlias("#room:matrix.org").toRoomIdOrAlias(),
eventId = null,
viaParameters = persistentListOf(),
)
else -> throw AssertionError("Unexpected value $uriString")
}
}
},
)
ElementPreview {
provider.setup()
val provider = remember {
MentionSpanProvider(
currentSessionId = "@me:matrix.org",
permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return when (uriString) {
"https://matrix.to/#/@me:matrix.org" -> PermalinkData.UserLink(UserId("@me:matrix.org"))
"https://matrix.to/#/@other:matrix.org" -> PermalinkData.UserLink(UserId("@other:matrix.org"))
"https://matrix.to/#/#room:matrix.org" -> PermalinkData.RoomLink(
roomIdOrAlias = RoomAlias("#room:matrix.org").toRoomIdOrAlias(),
eventId = null,
viaParameters = persistentListOf(),
)
else -> throw AssertionError("Unexpected value $uriString")
}
}
},
)
}
provider.updateStyles()
val textColor = ElementTheme.colors.textPrimary.toArgb()
fun mentionSpanMe() = provider.getMentionSpanFor("mention", "https://matrix.to/#/@me:matrix.org")
@ -199,3 +196,14 @@ internal fun MentionSpanPreview() {
})
}
}
val LocalMentionSpanProvider = staticCompositionLocalOf {
MentionSpanProvider(
currentSessionId = "@dummy:value.org",
permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return PermalinkData.FallbackLink(Uri.EMPTY)
}
},
)
}

View file

@ -98,7 +98,7 @@ class MarkdownTextEditorState(
replace(start, end, "@room")
} else {
val link = permalinkBuilder.permalinkForUser(UserId(mention.rawValue)).getOrNull() ?: continue
replace(start, end, "[${mention.text}]($link)")
replace(start, end, "[${mention.rawValue}]($link)")
}
}
}

View file

@ -164,7 +164,7 @@ class MarkdownTextInputTest {
editor = it.findEditor()
state.insertMention(
ResolvedMentionSuggestion.Member(roomMember = aRoomMember()),
MentionSpanProvider(currentSessionId = A_SESSION_ID, permalinkParser = permalinkParser),
MentionSpanProvider(currentSessionId = A_SESSION_ID.value, permalinkParser = permalinkParser),
permalinkBuilder,
)
}

View file

@ -43,13 +43,14 @@ class MentionSpanProviderTest {
private val permalinkParser = FakePermalinkParser()
private val mentionSpanProvider = MentionSpanProvider(
currentSessionId = currentUserId,
currentSessionId = currentUserId.value,
permalinkParser = permalinkParser,
currentUserBackgroundColor = myUserColor,
currentUserTextColor = myUserColor,
otherBackgroundColor = otherColor,
otherTextColor = otherColor,
)
).apply {
currentUserBackgroundColor = myUserColor
currentUserTextColor = myUserColor
otherBackgroundColor = otherColor
otherTextColor = otherColor
}
@Test
fun `getting mention span for current user should return a MentionSpan with custom colors`() {

View file

@ -124,7 +124,7 @@ class MarkdownTextEditorStateTest {
val markdown = state.getMessageMarkdown(permalinkBuilder = permalinkBuilder)
assertThat(markdown).isEqualTo(
"Hello [@Alice](https://matrix.to/#/@alice:matrix.org) and everyone in @room"
"Hello [@alice:matrix.org](https://matrix.to/#/@alice:matrix.org) and everyone in @room"
)
}
@ -151,7 +151,7 @@ class MarkdownTextEditorStateTest {
currentSessionId: SessionId = A_SESSION_ID,
permalinkParser: FakePermalinkParser = FakePermalinkParser(),
): MentionSpanProvider {
return MentionSpanProvider(currentSessionId, permalinkParser)
return MentionSpanProvider(currentSessionId.value, permalinkParser)
}
private fun aMarkdownTextWithMentions(): CharSequence {