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
commit 5551f4e039
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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,
)
}

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71acb17375db16b8777221e3a09c85ce821920dfaf7853c456493371e4778b9d
size 37050
oid sha256:85a7f1e86ee77355ada16764c0cdaa96193237e1e0936188b00b99247b67f272
size 39232

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b03d25813c75712a0b9e23622132bd63e07ca9ec593a1842dacf751347c06ea1
size 32023
oid sha256:507eec1e5125056ca4980ae67f904dd30bcb88ef6d66d5968aafbb3c1449e4b8
size 33779

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3aaf6da43b5f2882da986ae766d0057421b1f39f7f0bb467bd32f2a957608045
size 31478
oid sha256:d300a9854096634ea0e6722db487607a75f6e0c46d7341f044a190a212d95307
size 32373

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72be11ed273a790b0ab8e5023c1e21384c020e1b0fc0e3950de8f041c4004e64
size 33758
oid sha256:b42e3df9fec259210b63431e295e8c7d1a7640149c9616b572b90c0b40392a60
size 34637

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5343bc348bc1be7430243ce6fceeb7c81bbf2d5692486733221cb6c615d9aeea
size 41544
oid sha256:3bb107a37b14dcbc6aa530a8d47f58c809df7719a0c8a416a268ffd2df165681
size 41602

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9cf6cd1f914a4f31608edceefde79632ebed0c8983d83d8a63c4408bcc2d4ff3
size 56743
oid sha256:6a2d21173c63d0b9b3a558d23acd380c9d15fe5c4dc1b62f31644817318e049f
size 56061

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00a667f59cd097021a6d363d0e4f578cba6acc405aaaa74bddfa9a3286cfd26e
size 56408
oid sha256:89598f3f1149cec915196f1abb1998855c3dd161d6f05f32bccc7bb85cd1938a
size 55715

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5bdcf84378968b8e37cc50a300ff19b1917354de764a9f81448e6ca3d4050386
size 54711
oid sha256:8ae448a2c6f5da0b563873a683884d75a74ded6157edc56e91507a1857ff87ee
size 56298

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:577eac69d32648070d896788474104fdf143c040e058f182d6b832e5ea83fad8
size 37242
oid sha256:b67ebf4536e36571b0867e71533b2fe1953ca2b88f15fe3531f3ddb9043f69ec
size 39418

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:745a270883c79932fdde14d11785d9e27d1ec0eee03999af5e494b5c6e30c309
size 57329
oid sha256:e5fb16ef839b9f93caddbc984c8be260846e7fd7d68ae3b31dcc97c7644b41b4
size 56670

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:451eee79c5ca4b61902044c58f99069065fcd11a9fa7d469d2568cd20ea134a3
size 56743
oid sha256:9402ad99b181b99083abff78d078e9fbb010144e07f80ff995db7884a61fa853
size 56060

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:023b2870f5a4291ad8b521dc177db22972a47e5e965458c81cb8688da0cbe6b8
size 57097
oid sha256:cadacec9344f3ca2c029006c620cd8c309bb7f4b38cbb7b47c7c336ca265a141
size 56424

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1463d9c6b2d968c0a5f375e5135b31e31b32f2d35525e0be1c264d90ff4e723c
size 38652
oid sha256:cc24db5d5659ddee2787ffc6a3371351478c9ccd7cfb909ede6a06f1e72a907a
size 40756

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:09a88867a2f10ce346df8d683d614970f9aad8e8ec7bf06bddf27352dc0e5476
size 33520
oid sha256:548950d915f6fbfdf1a17b81619d3d4cbefe93b79091d259bcec41ae6bbbc652
size 35422

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81162b1f086e45797db0678eff6bc41ad35eb09a7ac436d540fd3b7e1feffbc6
size 33534
oid sha256:6b47bcab08cded6e857fe213ab2c90602e557a36feebb45b2b8d4d18115ac68b
size 34582

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1cbcce698faad80969cd00d2680afd7cd61b6ba649f0b67e8c7fb2fe0dbe746
size 35884
oid sha256:5cd5de47e285702bf3d2ee325fc83a1168a0367d9c6b54018565daa0ed503a77
size 36876

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b0ce43031e71ffadc53388a66a9ceb66b1c9bb33014a912161ec6006a303050b
size 43544
oid sha256:59b6d7d5e484b34e1ce7814432144df34b1cd3323d31c90db439cbb5d9f8dc83
size 43588

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a906ec0c412892a241eab241836d4238c1883ec1d27f27b8a012de75894fdaf7
size 58785
oid sha256:01cab7fd5a8b4e10a4ecca8a31b0f659fb9f2db9260272b0f6c96c4e126050aa
size 57895

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e4ccb39f3024e3a98be3bc0f94ce769e412b891ed988dad4cf1457aab3cea72
size 58442
oid sha256:18e03a178666b24e95a9cd0ae5d95d0fe57db49d11b1b1f3cae5ed472e94b6d7
size 57550

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8b10af4e5006f45e2fe738a3a8a589551378ee9106c09f10b9de81fdbf12e61
size 56716
oid sha256:f4aee340402c5fa0cdcaf2603ae5999f353eec734a1ab928c242889a1d5d2425
size 58272

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc82d2ab3bc9d73726bcc0cdc5dc27b628d00db330cfcc7a78f177af029a7803
size 38810
oid sha256:a3bebec99f2b870e7a38333590f5cb776b30f4f9b976d99601a7fb3bfd05a71f
size 40913

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aef64f63cb176feea3ee1e48de98f7bf6ed104370f84e377c963a2f26ba0fa49
size 59387
oid sha256:853218e7ddc4d18b260f6ab047beb82778284851e230babc14113c6cb329d29a
size 58504

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e6f46d940ee39961ea128fc7fc607dd6156d827121def3516ef62ff5f617e249
size 58785
oid sha256:e7caee4430244e294b75e3b6a5830088dd25397b1c272666eb2bec47ecd1f382
size 57895

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ea58c892886c9de971fd22a5a72340775cb0f973cdfb697d7f517100ce8b65d
size 59205
oid sha256:bbb4224047c88d838de227e61332775e5efe0ce1380a7446c701f0db5b2d2dcc
size 58323