Browse Source
* Restore intentional mentions in the markdown/plain text editor --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>pull/3200/head
Jorge Martin Espinosa
2 months ago
committed by
GitHub
31 changed files with 715 additions and 286 deletions
@ -0,0 +1,80 @@ |
|||||||
|
/* |
||||||
|
* 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 |
||||||
|
* |
||||||
|
* https://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.features.messages.impl.utils |
||||||
|
|
||||||
|
import android.text.Spannable |
||||||
|
import android.text.SpannableStringBuilder |
||||||
|
import androidx.core.text.getSpans |
||||||
|
import io.element.android.libraries.matrix.api.core.MatrixPatternType |
||||||
|
import io.element.android.libraries.matrix.api.core.MatrixPatterns |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias |
||||||
|
import io.element.android.libraries.matrix.api.core.UserId |
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder |
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser |
||||||
|
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache |
||||||
|
import io.element.android.libraries.textcomposer.mentions.MentionSpan |
||||||
|
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
class TextPillificationHelper @Inject constructor( |
||||||
|
private val mentionSpanProvider: MentionSpanProvider, |
||||||
|
private val permalinkBuilder: PermalinkBuilder, |
||||||
|
private val permalinkParser: PermalinkParser, |
||||||
|
private val roomMemberProfilesCache: RoomMemberProfilesCache, |
||||||
|
) { |
||||||
|
@Suppress("LoopWithTooManyJumpStatements") |
||||||
|
fun pillify(text: CharSequence): CharSequence { |
||||||
|
val matches = MatrixPatterns.findPatterns(text, permalinkParser).sortedByDescending { it.end } |
||||||
|
if (matches.isEmpty()) return text |
||||||
|
|
||||||
|
val spannable = SpannableStringBuilder(text) |
||||||
|
for (match in matches) { |
||||||
|
when (match.type) { |
||||||
|
MatrixPatternType.USER_ID -> { |
||||||
|
val mentionSpanExists = spannable.getSpans<MentionSpan>(match.start, match.end).isNotEmpty() |
||||||
|
if (!mentionSpanExists) { |
||||||
|
val userId = UserId(match.value) |
||||||
|
val permalink = permalinkBuilder.permalinkForUser(userId).getOrNull() ?: continue |
||||||
|
val mentionSpan = mentionSpanProvider.getMentionSpanFor(match.value, permalink) |
||||||
|
roomMemberProfilesCache.getDisplayName(userId)?.let { mentionSpan.text = it } |
||||||
|
spannable.replace(match.start, match.end, "@ ") |
||||||
|
spannable.setSpan(mentionSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) |
||||||
|
} |
||||||
|
} |
||||||
|
MatrixPatternType.ROOM_ALIAS -> { |
||||||
|
val mentionSpanExists = spannable.getSpans<MentionSpan>(match.start, match.end).isNotEmpty() |
||||||
|
if (!mentionSpanExists) { |
||||||
|
val permalink = permalinkBuilder.permalinkForRoomAlias(RoomAlias(match.value)).getOrNull() ?: continue |
||||||
|
val mentionSpan = mentionSpanProvider.getMentionSpanFor(match.value, permalink) |
||||||
|
spannable.replace(match.start, match.end, "@ ") |
||||||
|
spannable.setSpan(mentionSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) |
||||||
|
} |
||||||
|
} |
||||||
|
MatrixPatternType.AT_ROOM -> { |
||||||
|
val mentionSpanExists = spannable.getSpans<MentionSpan>(match.start, match.end).isNotEmpty() |
||||||
|
if (!mentionSpanExists) { |
||||||
|
val mentionSpan = mentionSpanProvider.getMentionSpanFor("@room", "") |
||||||
|
spannable.replace(match.start, match.end, "@ ") |
||||||
|
spannable.setSpan(mentionSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) |
||||||
|
} |
||||||
|
} |
||||||
|
else -> Unit |
||||||
|
} |
||||||
|
} |
||||||
|
return spannable |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,128 @@ |
|||||||
|
/* |
||||||
|
* 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 |
||||||
|
* |
||||||
|
* https://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.features.messages.impl.utils |
||||||
|
|
||||||
|
import android.net.Uri |
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||||
|
import com.google.common.truth.Truth.assertThat |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias |
||||||
|
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.test.permalink.FakePermalinkBuilder |
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser |
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember |
||||||
|
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache |
||||||
|
import io.element.android.libraries.textcomposer.mentions.MentionSpan |
||||||
|
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider |
||||||
|
import io.element.android.libraries.textcomposer.mentions.getMentionSpans |
||||||
|
import org.junit.Test |
||||||
|
import org.junit.runner.RunWith |
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class) |
||||||
|
class TextPillificationHelperTest { |
||||||
|
@Test |
||||||
|
fun `pillify - adds pills for user ids`() { |
||||||
|
val text = "A @user:server.com" |
||||||
|
val helper = aTextPillificationHelper( |
||||||
|
permalinkparser = FakePermalinkParser(result = { |
||||||
|
PermalinkData.UserLink(UserId("@user:server.com")) |
||||||
|
}), |
||||||
|
permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { |
||||||
|
Result.success("https://matrix.to/#/@user:server.com") |
||||||
|
}), |
||||||
|
) |
||||||
|
val pillified = helper.pillify(text) |
||||||
|
val mentionSpans = pillified.getMentionSpans() |
||||||
|
assertThat(mentionSpans).hasSize(1) |
||||||
|
val mentionSpan = mentionSpans.firstOrNull() |
||||||
|
assertThat(mentionSpan?.type).isEqualTo(MentionSpan.Type.USER) |
||||||
|
assertThat(mentionSpan?.rawValue).isEqualTo("@user:server.com") |
||||||
|
assertThat(mentionSpan?.text).isEqualTo("@user:server.com") |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `pillify - uses the cached display name for user mentions`() { |
||||||
|
val text = "A @user:server.com" |
||||||
|
val helper = aTextPillificationHelper( |
||||||
|
permalinkparser = FakePermalinkParser(result = { |
||||||
|
PermalinkData.UserLink(UserId("@user:server.com")) |
||||||
|
}), |
||||||
|
permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { |
||||||
|
Result.success("https://matrix.to/#/@user:server.com") |
||||||
|
}), |
||||||
|
roomMemberProfilesCache = RoomMemberProfilesCache().apply { |
||||||
|
replace(listOf(aRoomMember(userId = UserId("@user:server.com"), displayName = "Alice"))) |
||||||
|
}, |
||||||
|
) |
||||||
|
val pillified = helper.pillify(text) |
||||||
|
val mentionSpans = pillified.getMentionSpans() |
||||||
|
assertThat(mentionSpans).hasSize(1) |
||||||
|
val mentionSpan = mentionSpans.firstOrNull() |
||||||
|
assertThat(mentionSpan?.type).isEqualTo(MentionSpan.Type.USER) |
||||||
|
assertThat(mentionSpan?.rawValue).isEqualTo("@user:server.com") |
||||||
|
assertThat(mentionSpan?.text).isEqualTo("Alice") |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `pillify - adds pills for room aliases`() { |
||||||
|
val text = "A #room:server.com" |
||||||
|
val helper = aTextPillificationHelper( |
||||||
|
permalinkparser = FakePermalinkParser(result = { |
||||||
|
PermalinkData.RoomLink(RoomIdOrAlias.Alias(RoomAlias("#room:server.com"))) |
||||||
|
}), |
||||||
|
permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { |
||||||
|
Result.success("https://matrix.to/#/#room:server.com") |
||||||
|
}), |
||||||
|
) |
||||||
|
val pillified = helper.pillify(text) |
||||||
|
val mentionSpans = pillified.getMentionSpans() |
||||||
|
assertThat(mentionSpans).hasSize(1) |
||||||
|
val mentionSpan = mentionSpans.firstOrNull() |
||||||
|
assertThat(mentionSpan?.type).isEqualTo(MentionSpan.Type.ROOM) |
||||||
|
assertThat(mentionSpan?.rawValue).isEqualTo("#room:server.com") |
||||||
|
assertThat(mentionSpan?.text).isEqualTo("#room:server.com") |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `pillify - adds pills for @room mentions`() { |
||||||
|
val text = "An @room mention" |
||||||
|
val helper = aTextPillificationHelper(permalinkparser = FakePermalinkParser(result = { |
||||||
|
PermalinkData.FallbackLink(Uri.EMPTY) |
||||||
|
})) |
||||||
|
val pillified = helper.pillify(text) |
||||||
|
val mentionSpans = pillified.getMentionSpans() |
||||||
|
assertThat(mentionSpans).hasSize(1) |
||||||
|
val mentionSpan = mentionSpans.firstOrNull() |
||||||
|
assertThat(mentionSpan?.type).isEqualTo(MentionSpan.Type.EVERYONE) |
||||||
|
assertThat(mentionSpan?.rawValue).isEqualTo("@room") |
||||||
|
assertThat(mentionSpan?.text).isEqualTo("@room") |
||||||
|
} |
||||||
|
|
||||||
|
private fun aTextPillificationHelper( |
||||||
|
permalinkparser: PermalinkParser = FakePermalinkParser(), |
||||||
|
permalinkBuilder: FakePermalinkBuilder = FakePermalinkBuilder(), |
||||||
|
mentionSpanProvider: MentionSpanProvider = MentionSpanProvider(permalinkparser), |
||||||
|
roomMemberProfilesCache: RoomMemberProfilesCache = RoomMemberProfilesCache(), |
||||||
|
) = TextPillificationHelper( |
||||||
|
mentionSpanProvider = mentionSpanProvider, |
||||||
|
permalinkBuilder = permalinkBuilder, |
||||||
|
permalinkParser = permalinkparser, |
||||||
|
roomMemberProfilesCache = roomMemberProfilesCache, |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
/* |
||||||
|
* 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 |
||||||
|
* |
||||||
|
* https://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.api.core |
||||||
|
|
||||||
|
import android.net.Uri |
||||||
|
import com.google.common.truth.Truth.assertThat |
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData |
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser |
||||||
|
import org.junit.Test |
||||||
|
|
||||||
|
class MatrixPatternsTest { |
||||||
|
@Test |
||||||
|
fun `findPatterns - returns raw user ids`() { |
||||||
|
val text = "A @user:server.com and @user2:server.com" |
||||||
|
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser()) |
||||||
|
assertThat(patterns).containsExactly( |
||||||
|
MatrixPatternResult(MatrixPatternType.USER_ID, "@user:server.com", 2, 18), |
||||||
|
MatrixPatternResult(MatrixPatternType.USER_ID, "@user2:server.com", 23, 40) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `findPatterns - returns raw room ids`() { |
||||||
|
val text = "A !room:server.com and !room2:server.com" |
||||||
|
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser()) |
||||||
|
assertThat(patterns).containsExactly( |
||||||
|
MatrixPatternResult(MatrixPatternType.ROOM_ID, "!room:server.com", 2, 18), |
||||||
|
MatrixPatternResult(MatrixPatternType.ROOM_ID, "!room2:server.com", 23, 40) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `findPatterns - returns raw room aliases`() { |
||||||
|
val text = "A #room:server.com and #room2:server.com" |
||||||
|
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser()) |
||||||
|
assertThat(patterns).containsExactly( |
||||||
|
MatrixPatternResult(MatrixPatternType.ROOM_ALIAS, "#room:server.com", 2, 18), |
||||||
|
MatrixPatternResult(MatrixPatternType.ROOM_ALIAS, "#room2:server.com", 23, 40) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `findPatterns - returns raw room event ids`() { |
||||||
|
val text = "A \$event:server.com and \$event2:server.com" |
||||||
|
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser()) |
||||||
|
assertThat(patterns).containsExactly( |
||||||
|
MatrixPatternResult(MatrixPatternType.EVENT_ID, "\$event:server.com", 2, 19), |
||||||
|
MatrixPatternResult(MatrixPatternType.EVENT_ID, "\$event2:server.com", 24, 42) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `findPatterns - returns @room mention`() { |
||||||
|
val text = "A @room mention" |
||||||
|
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser()) |
||||||
|
assertThat(patterns).containsExactly(MatrixPatternResult(MatrixPatternType.AT_ROOM, "@room", 2, 7)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `findPatterns - returns user ids in permalinks`() { |
||||||
|
val text = "A [User](https://matrix.to/#/@user:server.com)" |
||||||
|
val permalinkParser = aPermalinkParser { _ -> |
||||||
|
PermalinkData.UserLink(UserId("@user:server.com")) |
||||||
|
} |
||||||
|
val patterns = MatrixPatterns.findPatterns(text, permalinkParser) |
||||||
|
assertThat(patterns).containsExactly(MatrixPatternResult(MatrixPatternType.USER_ID, "@user:server.com", 2, 46)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `findPatterns - returns room aliases in permalinks`() { |
||||||
|
val text = "A [Room](https://matrix.to/#/#room:server.com)" |
||||||
|
val permalinkParser = aPermalinkParser { _ -> |
||||||
|
PermalinkData.RoomLink(RoomIdOrAlias.Alias(RoomAlias("#room:server.com"))) |
||||||
|
} |
||||||
|
val patterns = MatrixPatterns.findPatterns(text, permalinkParser) |
||||||
|
assertThat(patterns).containsExactly(MatrixPatternResult(MatrixPatternType.ROOM_ALIAS, "#room:server.com", 2, 46)) |
||||||
|
} |
||||||
|
|
||||||
|
private fun aPermalinkParser(block: (String) -> PermalinkData = { PermalinkData.FallbackLink(Uri.EMPTY) }) = object : PermalinkParser { |
||||||
|
override fun parse(uriString: String): PermalinkData { |
||||||
|
return block(uriString) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,172 @@ |
|||||||
|
/* |
||||||
|
* 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 |
||||||
|
* |
||||||
|
* https://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.textcomposer.mentions |
||||||
|
|
||||||
|
import android.graphics.Color |
||||||
|
import android.graphics.Typeface |
||||||
|
import android.text.Spanned |
||||||
|
import android.view.ViewGroup |
||||||
|
import android.widget.TextView |
||||||
|
import androidx.compose.foundation.layout.PaddingValues |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.CompositionLocalProvider |
||||||
|
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 io.element.android.compound.theme.ElementTheme |
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight |
||||||
|
import io.element.android.libraries.designsystem.text.rememberTypeface |
||||||
|
import io.element.android.libraries.designsystem.theme.currentUserMentionPillBackground |
||||||
|
import io.element.android.libraries.designsystem.theme.currentUserMentionPillText |
||||||
|
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.UserId |
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias |
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData |
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser |
||||||
|
import kotlinx.collections.immutable.persistentListOf |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
/** |
||||||
|
* Theme used for mention spans. |
||||||
|
* To make this work, you need to: |
||||||
|
* 1. Provide [LocalMentionSpanTheme] in a composable that wraps the ones where you want to use mentions. |
||||||
|
* 2. Call [MentionSpanTheme.updateStyles] with the current [UserId] so the colors and sizes are computed. |
||||||
|
* 3. Use either [MentionSpanTheme.updateMentionStyles] or [MentionSpan.update] to update the styles of the mention spans. |
||||||
|
*/ |
||||||
|
@Stable |
||||||
|
class MentionSpanTheme @Inject constructor() { |
||||||
|
internal var currentUserId: UserId? = null |
||||||
|
internal var currentUserTextColor: Int = 0 |
||||||
|
internal var currentUserBackgroundColor: Int = Color.WHITE |
||||||
|
internal var otherTextColor: Int = 0 |
||||||
|
internal var otherBackgroundColor: Int = Color.WHITE |
||||||
|
|
||||||
|
private val paddingValues = PaddingValues(start = 4.dp, end = 6.dp) |
||||||
|
internal val paddingValuesPx = mutableStateOf(0 to 0) |
||||||
|
internal val typeface = mutableStateOf(Typeface.DEFAULT) |
||||||
|
|
||||||
|
/** |
||||||
|
* Updates the styles of the mention spans based on the [ElementTheme] and [currentUserId]. |
||||||
|
*/ |
||||||
|
@Suppress("ComposableNaming") |
||||||
|
@Composable |
||||||
|
fun updateStyles(currentUserId: UserId) { |
||||||
|
this.currentUserId = currentUserId |
||||||
|
currentUserTextColor = ElementTheme.colors.currentUserMentionPillText.toArgb() |
||||||
|
currentUserBackgroundColor = ElementTheme.colors.currentUserMentionPillBackground.toArgb() |
||||||
|
otherTextColor = ElementTheme.colors.mentionPillText.toArgb() |
||||||
|
otherBackgroundColor = ElementTheme.colors.mentionPillBackground.toArgb() |
||||||
|
|
||||||
|
typeface.value = ElementTheme.typography.fontBodyLgMedium.rememberTypeface().value |
||||||
|
val density = LocalDensity.current |
||||||
|
val layoutDirection = LocalLayoutDirection.current |
||||||
|
paddingValuesPx.value = remember(paddingValues, density, layoutDirection) { |
||||||
|
with(density) { |
||||||
|
val leftPadding = paddingValues.calculateLeftPadding(layoutDirection).roundToPx() |
||||||
|
val rightPadding = paddingValues.calculateRightPadding(layoutDirection).roundToPx() |
||||||
|
leftPadding to rightPadding |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Updates the styles of the mention spans in the given [CharSequence]. |
||||||
|
*/ |
||||||
|
fun MentionSpanTheme.updateMentionStyles(charSequence: CharSequence) { |
||||||
|
val spanned = charSequence as? Spanned ?: return |
||||||
|
val mentionSpans = spanned.getMentionSpans() |
||||||
|
for (span in mentionSpans) { |
||||||
|
span.update(this) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Composition local containing the current [MentionSpanTheme]. |
||||||
|
*/ |
||||||
|
val LocalMentionSpanTheme = staticCompositionLocalOf { |
||||||
|
MentionSpanTheme() |
||||||
|
} |
||||||
|
|
||||||
|
@PreviewsDayNight |
||||||
|
@Composable |
||||||
|
internal fun MentionSpanThemePreview() { |
||||||
|
ElementPreview { |
||||||
|
val mentionSpanTheme = remember { MentionSpanTheme() } |
||||||
|
val provider = remember { |
||||||
|
MentionSpanProvider( |
||||||
|
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") |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
val textColor = ElementTheme.colors.textPrimary.toArgb() |
||||||
|
fun mentionSpanMe() = provider.getMentionSpanFor("mention", "https://matrix.to/#/@me:matrix.org") |
||||||
|
fun mentionSpanOther() = provider.getMentionSpanFor("mention", "https://matrix.to/#/@other:matrix.org") |
||||||
|
fun mentionSpanRoom() = provider.getMentionSpanFor("room", "https://matrix.to/#/#room:matrix.org") |
||||||
|
mentionSpanTheme.updateStyles(currentUserId = UserId("@me:matrix.org")) |
||||||
|
|
||||||
|
CompositionLocalProvider( |
||||||
|
LocalMentionSpanTheme provides mentionSpanTheme |
||||||
|
) { |
||||||
|
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", mentionSpanMe(), 0) |
||||||
|
append(" to the current user and this is a ") |
||||||
|
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 '#'.") |
||||||
|
} |
||||||
|
mentionSpanTheme.updateMentionStyles(text) |
||||||
|
setTextColor(textColor) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue