diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/historyvisible/HistoryVisibleStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/historyvisible/HistoryVisibleStateView.kt index 9c08c9d101..d0655f695d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/historyvisible/HistoryVisibleStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/historyvisible/HistoryVisibleStateView.kt @@ -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) }, ) diff --git a/features/securityandprivacy/impl/build.gradle.kts b/features/securityandprivacy/impl/build.gradle.kts index 5a83cebd8c..bb9764eb60 100644 --- a/features/securityandprivacy/impl/build.gradle.kts +++ b/features/securityandprivacy/impl/build.gradle.kts @@ -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) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt index e173117431..d5fb72e72e 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyNode.kt @@ -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 ) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index 472f71ddfb..e627fef40c 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -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 } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index 2c6bd59e12..7f43d4e94a 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -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. @@ -23,19 +23,24 @@ data class SecurityAndPrivacyState( val isKnockEnabled: Boolean, val saveAction: AsyncAction, val isSpace: Boolean, + private val permissions: SecurityAndPrivacyPermissions, val eventSink: (SecurityAndPrivacyEvent) -> Unit ) { 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 +60,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 } } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index b5347a753f..223d524ca3 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -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 = AsyncData.Uninitialized, ) = SecurityAndPrivacySettings( roomAccess = roomAccess, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt index 125d84c84d..bf6a5ffdf2 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt @@ -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, + availableOptions: ImmutableList, 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 = {}, ) } diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt index 5f3d14958c..25331470b6 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyPresenterTest.kt @@ -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.Shared) 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.WorldReadable)) } with(awaitItem()) { - assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection) + assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable) 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) diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt index 6b429c04ea..66178878f5 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyViewTest.kt @@ -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 @@ -146,12 +147,12 @@ class SecurityAndPrivacyViewTest { 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 @@ -184,10 +185,12 @@ private fun AndroidComposeTestRule.setSecur state: SecurityAndPrivacyState = aSecurityAndPrivacyState( eventSink = EventsRecorder(expectEvents = false), ), + onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { SecurityAndPrivacyView( state = state, + onLinkClick = onLinkClick, ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/StringWithLink.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/StringWithLink.kt new file mode 100644 index 0000000000..d82ea9e817 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/StringWithLink.kt @@ -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, + ) +}