Merge pull request #5942 from element-hq/feature/bma/roomHistoryVisibilitySettings

Simplify the copy of the history visibility settings
This commit is contained in:
Benoit Marty
2025-12-20 17:02:24 +01:00
committed by GitHub
36 changed files with 199 additions and 127 deletions

View File

@@ -10,18 +10,13 @@ package io.element.android.features.messages.impl.crypto.historyvisible
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.appconfig.LearnMoreConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertLevel
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.stringWithLink
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
@@ -33,37 +28,16 @@ fun HistoryVisibleStateView(
if (!state.showAlert) {
return
}
ComposerAlertMolecule(
modifier = modifier,
avatar = null,
showIcon = true,
level = ComposerAlertLevel.Info,
content = buildAnnotatedString {
val learnMoreStr = stringResource(CommonStrings.action_learn_more)
val fullText = stringResource(CommonStrings.crypto_history_visible, learnMoreStr)
append(fullText)
val learnMoreStartIndex = fullText.lastIndexOf(learnMoreStr)
addStyle(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
fontWeight = FontWeight.Bold,
color = ElementTheme.colors.textPrimary
),
start = learnMoreStartIndex,
end = learnMoreStartIndex + learnMoreStr.length,
)
addLink(
url = LinkAnnotation.Url(
url = LearnMoreConfig.HISTORY_VISIBLE_URL,
linkInteractionListener = {
onLinkClick(LearnMoreConfig.HISTORY_VISIBLE_URL, true)
}
),
start = learnMoreStartIndex,
end = learnMoreStartIndex + learnMoreStr.length,
)
},
content = stringWithLink(
textRes = CommonStrings.crypto_history_visible,
url = LearnMoreConfig.HISTORY_VISIBLE_URL,
onLinkClick = { url -> onLinkClick(url, true) },
),
submitText = stringResource(CommonStrings.action_dismiss),
onSubmitClick = { state.eventSink(HistoryVisibleEvent.Acknowledge) },
)

View File

@@ -157,10 +157,11 @@ We do not recommend enabling encryption for rooms that anyone can find and join.
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Allow for this room to be found by searching %1$s public room directory"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Allow to be found by searching the public directory."</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Visible in public directory"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Anyone"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Anyone (history is public)"</string>
<string name="screen_security_and_privacy_room_history_section_footer">"Changes won\'t affect past messages, only new ones. %1$s"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Who can read history"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Members only since they were invited"</string>
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Members only since selecting this option"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Members since invited"</string>
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Members (full history)"</string>
<string name="screen_security_and_privacy_room_publishing_section_footer">"Room addresses are ways to find and access rooms. This also ensures you can easily share your room with others.
You can choose to publish your room in your homeserver public room directory."</string>
<string name="screen_security_and_privacy_room_publishing_section_header">"Room publishing"</string>

View File

@@ -28,7 +28,9 @@ setupDependencyInjection()
dependencies {
api(projects.features.securityandprivacy.api)
implementation(projects.appconfig)
implementation(projects.appnav)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.designsystem)

View File

@@ -8,6 +8,8 @@
package io.element.android.features.securityandprivacy.impl.root
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -19,7 +21,9 @@ import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.di.RoomScope
@@ -35,11 +39,20 @@ class SecurityAndPrivacyNode(
private val stateFlow = launchMolecule { presenter.present() }
private fun onOpenExternalUrl(activity: Activity, darkTheme: Boolean, url: String) {
activity.openUrlInChromeCustomTab(null, darkTheme, url)
}
@Composable
override fun View(modifier: Modifier) {
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
val state by stateFlow.collectAsState()
SecurityAndPrivacyView(
state = state,
onLinkClick = { url ->
onOpenExternalUrl(activity, isDark, url)
},
modifier = modifier
)
}

View File

@@ -301,21 +301,21 @@ private fun SecurityAndPrivacyRoomAccess.map(): JoinRule? {
private fun RoomHistoryVisibility?.map(): SecurityAndPrivacyHistoryVisibility {
return when (this) {
RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone
RoomHistoryVisibility.Joined,
RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite
RoomHistoryVisibility.Shared -> SecurityAndPrivacyHistoryVisibility.SinceSelection
// All other cases are not supported so we default to SinceSelection
RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.Invited
RoomHistoryVisibility.Shared -> SecurityAndPrivacyHistoryVisibility.Shared
RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.WorldReadable
// All other cases are not supported so we default to Shared
is RoomHistoryVisibility.Custom,
null -> SecurityAndPrivacyHistoryVisibility.SinceSelection
null -> SecurityAndPrivacyHistoryVisibility.Shared
}
}
private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility {
return when (this) {
SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared
SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited
SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable
SecurityAndPrivacyHistoryVisibility.Invited -> RoomHistoryVisibility.Invited
SecurityAndPrivacyHistoryVisibility.Shared -> RoomHistoryVisibility.Shared
SecurityAndPrivacyHistoryVisibility.WorldReadable -> RoomHistoryVisibility.WorldReadable
}
}

View File

@@ -11,7 +11,7 @@ package io.element.android.features.securityandprivacy.impl.root
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.collections.immutable.toImmutableList
data class SecurityAndPrivacyState(
// the settings that are currently applied on the room.
@@ -28,14 +28,18 @@ data class SecurityAndPrivacyState(
) {
val canBeSaved = savedSettings != editedSettings
val availableHistoryVisibilities = buildSet {
add(SecurityAndPrivacyHistoryVisibility.SinceSelection)
// Logic is in https://github.com/element-hq/element-meta/issues/3029
val availableHistoryVisibilities = buildList {
// Shared is always available
add(SecurityAndPrivacyHistoryVisibility.Shared)
if (editedSettings.roomAccess == SecurityAndPrivacyRoomAccess.Anyone && !editedSettings.isEncrypted) {
add(SecurityAndPrivacyHistoryVisibility.Anyone)
add(SecurityAndPrivacyHistoryVisibility.WorldReadable)
} else {
add(SecurityAndPrivacyHistoryVisibility.SinceInvite)
add(SecurityAndPrivacyHistoryVisibility.Invited)
}
}.toImmutableSet()
}
.sorted()
.toImmutableList()
val showRoomAccessSection = permissions.canChangeRoomAccess
@@ -55,18 +59,19 @@ data class SecurityAndPrivacySettings(
)
enum class SecurityAndPrivacyHistoryVisibility {
SinceSelection,
SinceInvite,
Anyone;
// Order matters, and is from the most to the least restrictive
Invited,
Shared,
WorldReadable;
/**
* Returns the fallback visibility when the current visibility is not available.
*/
fun fallback(): SecurityAndPrivacyHistoryVisibility {
return when (this) {
SinceSelection,
SinceInvite -> SinceSelection
Anyone -> SinceInvite
Invited,
Shared -> Shared
WorldReadable -> Invited
}
}
}

View File

@@ -93,7 +93,7 @@ fun aSecurityAndPrivacySettings(
roomAccess: SecurityAndPrivacyRoomAccess = SecurityAndPrivacyRoomAccess.InviteOnly,
isEncrypted: Boolean = true,
address: String? = null,
historyVisibility: SecurityAndPrivacyHistoryVisibility = SecurityAndPrivacyHistoryVisibility.SinceSelection,
historyVisibility: SecurityAndPrivacyHistoryVisibility = SecurityAndPrivacyHistoryVisibility.Shared,
isVisibleInRoomDirectory: AsyncData<Boolean> = AsyncData.Uninitialized,
) = SecurityAndPrivacySettings(
roomAccess = roomAccess,

View File

@@ -27,8 +27,10 @@ import androidx.compose.material3.ListItemDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.appconfig.LearnMoreConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.securityandprivacy.impl.R
@@ -44,6 +46,7 @@ import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.text.stringWithLink
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
@@ -52,11 +55,12 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.ImmutableList
@Composable
fun SecurityAndPrivacyView(
state: SecurityAndPrivacyState,
onLinkClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler {
@@ -122,6 +126,7 @@ fun SecurityAndPrivacyView(
savedOptions = state.savedSettings.historyVisibility,
availableOptions = state.availableHistoryVisibilities,
onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(it)) },
onLinkClick = onLinkClick,
)
}
}
@@ -176,6 +181,7 @@ private fun SecurityAndPrivacyToolbar(
private fun SecurityAndPrivacySection(
title: String,
modifier: Modifier = Modifier,
subtitle: AnnotatedString? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Column(
@@ -187,6 +193,15 @@ private fun SecurityAndPrivacySection(
color = ElementTheme.colors.textPrimary,
modifier = Modifier.padding(horizontal = 16.dp),
)
if (subtitle != null) {
Spacer(Modifier.height(8.dp))
Text(
text = subtitle,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
content()
}
}
@@ -359,12 +374,18 @@ private fun EncryptionSection(
private fun HistoryVisibilitySection(
editedOption: SecurityAndPrivacyHistoryVisibility?,
savedOptions: SecurityAndPrivacyHistoryVisibility?,
availableOptions: ImmutableSet<SecurityAndPrivacyHistoryVisibility>,
availableOptions: ImmutableList<SecurityAndPrivacyHistoryVisibility>,
onSelectOption: (SecurityAndPrivacyHistoryVisibility) -> Unit,
onLinkClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
SecurityAndPrivacySection(
title = stringResource(R.string.screen_security_and_privacy_room_history_section_header),
subtitle = stringWithLink(
textRes = R.string.screen_security_and_privacy_room_history_section_footer,
url = LearnMoreConfig.HISTORY_VISIBLE_URL,
onLinkClick = onLinkClick,
),
modifier = modifier,
) {
for (availableOption in availableOptions) {
@@ -396,9 +417,9 @@ private fun HistoryVisibilityItem(
isEnabled: Boolean = true,
) {
val headlineText = when (option) {
SecurityAndPrivacyHistoryVisibility.SinceSelection -> stringResource(R.string.screen_security_and_privacy_room_history_since_selecting_option_title)
SecurityAndPrivacyHistoryVisibility.SinceInvite -> stringResource(R.string.screen_security_and_privacy_room_history_since_invite_option_title)
SecurityAndPrivacyHistoryVisibility.Anyone -> stringResource(R.string.screen_security_and_privacy_room_history_anyone_option_title)
SecurityAndPrivacyHistoryVisibility.Invited -> stringResource(R.string.screen_security_and_privacy_room_history_since_invite_option_title)
SecurityAndPrivacyHistoryVisibility.Shared -> stringResource(R.string.screen_security_and_privacy_room_history_since_selecting_option_title)
SecurityAndPrivacyHistoryVisibility.WorldReadable -> stringResource(R.string.screen_security_and_privacy_room_history_anyone_option_title)
}
ListItem(
headlineContent = { Text(text = headlineText) },
@@ -424,5 +445,6 @@ internal fun SecurityAndPrivacyViewDarkPreview(@PreviewParameter(SecurityAndPriv
private fun ContentToPreview(state: SecurityAndPrivacyState) {
SecurityAndPrivacyView(
state = state,
onLinkClick = {},
)
}

View File

@@ -31,10 +31,11 @@ We do not recommend enabling encryption for rooms that anyone can find and join.
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Allow for this room to be found by searching %1$s public room directory"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Allow to be found by searching the public directory."</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Visible in public directory"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Anyone"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Anyone (history is public)"</string>
<string name="screen_security_and_privacy_room_history_section_footer">"Changes won\'t affect past messages, only new ones. %1$s"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Who can read history"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Members only since they were invited"</string>
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Members only since selecting this option"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Members since invited"</string>
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Members (full history)"</string>
<string name="screen_security_and_privacy_room_publishing_section_footer">"Room addresses are ways to find and access rooms. This also ensures you can easily share your room with others.
You can choose to publish your room in your homeserver public room directory."</string>
<string name="screen_security_and_privacy_room_publishing_section_header">"Room publishing"</string>

View File

@@ -84,7 +84,7 @@ class SecurityAndPrivacyPresenterTest {
with(awaitItem()) {
assertThat(editedSettings).isEqualTo(savedSettings)
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone)
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone)
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable)
assertThat(editedSettings.address).isEqualTo(A_ROOM_ALIAS.value)
assertThat(canBeSaved).isFalse()
}
@@ -122,16 +122,16 @@ class SecurityAndPrivacyPresenterTest {
presenter.test {
skipItems(1)
with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection)
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceInvite))
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Shared)
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited))
}
with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceInvite)
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Invited)
assertThat(canBeSaved).isTrue()
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection))
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Shared))
}
with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection)
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Shared)
assertThat(canBeSaved).isFalse()
}
}
@@ -250,10 +250,10 @@ class SecurityAndPrivacyPresenterTest {
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
}
with(awaitItem()) {
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone))
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable))
}
with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone)
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable)
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
}
skipItems(1)
@@ -318,10 +318,10 @@ class SecurityAndPrivacyPresenterTest {
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
}
with(awaitItem()) {
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone))
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable))
}
with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone)
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable)
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
}
skipItems(1)

View File

@@ -24,6 +24,7 @@ import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPriv
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBack
@@ -140,22 +141,22 @@ class SecurityAndPrivacyViewTest {
}
@Test
@Config(qualifiers = "h640dp")
@Config(qualifiers = "h1024dp")
fun `click on history visibility item emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState(
eventSink = recorder,
editedSettings = aSecurityAndPrivacySettings(
historyVisibility = SecurityAndPrivacyHistoryVisibility.SinceSelection,
historyVisibility = SecurityAndPrivacyHistoryVisibility.Invited,
),
)
rule.setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_room_history_since_selecting_option_title)
recorder.assertSingle(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection))
rule.clickOn(R.string.screen_security_and_privacy_room_history_since_invite_option_title)
recorder.assertSingle(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited))
}
@Test
@Config(qualifiers = "h640dp")
@Config(qualifiers = "h1024dp")
fun `click on encryption item emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState(
@@ -184,10 +185,12 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecur
state: SecurityAndPrivacyState = aSecurityAndPrivacyState(
eventSink = EventsRecorder(expectEvents = false),
),
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
) {
setContent {
SecurityAndPrivacyView(
state = state,
onLinkClick = onLinkClick,
)
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.designsystem.text
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun stringWithLink(
@StringRes textRes: Int,
url: String,
onLinkClick: (String) -> Unit,
@StringRes linkTextRes: Int = CommonStrings.action_learn_more,
) = buildAnnotatedString {
val learnMoreStr = stringResource(linkTextRes)
val fullText = stringResource(textRes, learnMoreStr)
append(fullText)
val learnMoreStartIndex = fullText.lastIndexOf(learnMoreStr)
addStyle(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
fontWeight = FontWeight.Bold,
color = ElementTheme.colors.textPrimary
),
start = learnMoreStartIndex,
end = learnMoreStartIndex + learnMoreStr.length,
)
addLink(
url = LinkAnnotation.Url(
url = url,
linkInteractionListener = {
onLinkClick(url)
}
),
start = learnMoreStartIndex,
end = learnMoreStartIndex + learnMoreStr.length,
)
}