Simplify the copy of the history visibility settings

Closes #5898
This commit is contained in:
Benoit Marty
2025-12-18 11:00:10 +01:00
committed by Benoit Marty
parent 98f43f2402
commit d04ebe880b
10 changed files with 142 additions and 71 deletions

View File

@@ -10,18 +10,13 @@ package io.element.android.features.messages.impl.crypto.historyvisible
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.appconfig.LearnMoreConfig 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.ComposerAlertLevel
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.stringWithLink
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@@ -33,37 +28,16 @@ fun HistoryVisibleStateView(
if (!state.showAlert) { if (!state.showAlert) {
return return
} }
ComposerAlertMolecule( ComposerAlertMolecule(
modifier = modifier, modifier = modifier,
avatar = null, avatar = null,
showIcon = true, showIcon = true,
level = ComposerAlertLevel.Info, level = ComposerAlertLevel.Info,
content = buildAnnotatedString { content = stringWithLink(
val learnMoreStr = stringResource(CommonStrings.action_learn_more) textRes = CommonStrings.crypto_history_visible,
val fullText = stringResource(CommonStrings.crypto_history_visible, learnMoreStr) url = LearnMoreConfig.HISTORY_VISIBLE_URL,
append(fullText) onLinkClick = { url -> onLinkClick(url, true) },
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,
)
},
submitText = stringResource(CommonStrings.action_dismiss), submitText = stringResource(CommonStrings.action_dismiss),
onSubmitClick = { state.eventSink(HistoryVisibleEvent.Acknowledge) }, onSubmitClick = { state.eventSink(HistoryVisibleEvent.Acknowledge) },
) )

View File

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

View File

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

View File

@@ -301,21 +301,21 @@ private fun SecurityAndPrivacyRoomAccess.map(): JoinRule? {
private fun RoomHistoryVisibility?.map(): SecurityAndPrivacyHistoryVisibility { private fun RoomHistoryVisibility?.map(): SecurityAndPrivacyHistoryVisibility {
return when (this) { return when (this) {
RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone
RoomHistoryVisibility.Joined, RoomHistoryVisibility.Joined,
RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.Invited
RoomHistoryVisibility.Shared -> SecurityAndPrivacyHistoryVisibility.SinceSelection RoomHistoryVisibility.Shared -> SecurityAndPrivacyHistoryVisibility.Shared
// All other cases are not supported so we default to SinceSelection RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.WorldReadable
// All other cases are not supported so we default to Shared
is RoomHistoryVisibility.Custom, is RoomHistoryVisibility.Custom,
null -> SecurityAndPrivacyHistoryVisibility.SinceSelection null -> SecurityAndPrivacyHistoryVisibility.Shared
} }
} }
private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility {
return when (this) { return when (this) {
SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared SecurityAndPrivacyHistoryVisibility.Invited -> RoomHistoryVisibility.Invited
SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited SecurityAndPrivacyHistoryVisibility.Shared -> RoomHistoryVisibility.Shared
SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable 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.features.securityandprivacy.api.SecurityAndPrivacyPermissions
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableList
data class SecurityAndPrivacyState( data class SecurityAndPrivacyState(
// the settings that are currently applied on the room. // the settings that are currently applied on the room.
@@ -23,19 +23,24 @@ data class SecurityAndPrivacyState(
val isKnockEnabled: Boolean, val isKnockEnabled: Boolean,
val saveAction: AsyncAction<Unit>, val saveAction: AsyncAction<Unit>,
val isSpace: Boolean, val isSpace: Boolean,
private val permissions: SecurityAndPrivacyPermissions, private val permissions: SecurityAndPrivacyPermissions,
val eventSink: (SecurityAndPrivacyEvent) -> Unit val eventSink: (SecurityAndPrivacyEvent) -> Unit
) { ) {
val canBeSaved = savedSettings != editedSettings val canBeSaved = savedSettings != editedSettings
val availableHistoryVisibilities = buildSet { // Logic is in https://github.com/element-hq/element-meta/issues/3029
add(SecurityAndPrivacyHistoryVisibility.SinceSelection) val availableHistoryVisibilities = buildList {
// Shared is always available
add(SecurityAndPrivacyHistoryVisibility.Shared)
if (editedSettings.roomAccess == SecurityAndPrivacyRoomAccess.Anyone && !editedSettings.isEncrypted) { if (editedSettings.roomAccess == SecurityAndPrivacyRoomAccess.Anyone && !editedSettings.isEncrypted) {
add(SecurityAndPrivacyHistoryVisibility.Anyone) add(SecurityAndPrivacyHistoryVisibility.WorldReadable)
} else { } else {
add(SecurityAndPrivacyHistoryVisibility.SinceInvite) add(SecurityAndPrivacyHistoryVisibility.Invited)
} }
}.toImmutableSet() }
.sorted()
.toImmutableList()
val showRoomAccessSection = permissions.canChangeRoomAccess val showRoomAccessSection = permissions.canChangeRoomAccess
@@ -55,18 +60,19 @@ data class SecurityAndPrivacySettings(
) )
enum class SecurityAndPrivacyHistoryVisibility { enum class SecurityAndPrivacyHistoryVisibility {
SinceSelection, // Order matters, and is from the most to the least restrictive
SinceInvite, Invited,
Anyone; Shared,
WorldReadable;
/** /**
* Returns the fallback visibility when the current visibility is not available. * Returns the fallback visibility when the current visibility is not available.
*/ */
fun fallback(): SecurityAndPrivacyHistoryVisibility { fun fallback(): SecurityAndPrivacyHistoryVisibility {
return when (this) { return when (this) {
SinceSelection, Invited,
SinceInvite -> SinceSelection Shared -> Shared
Anyone -> SinceInvite WorldReadable -> Invited
} }
} }
} }

View File

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

View File

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

View File

@@ -84,7 +84,7 @@ class SecurityAndPrivacyPresenterTest {
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings).isEqualTo(savedSettings) assertThat(editedSettings).isEqualTo(savedSettings)
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) 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(editedSettings.address).isEqualTo(A_ROOM_ALIAS.value)
assertThat(canBeSaved).isFalse() assertThat(canBeSaved).isFalse()
} }
@@ -122,16 +122,16 @@ class SecurityAndPrivacyPresenterTest {
presenter.test { presenter.test {
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Shared)
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceInvite)) eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceInvite) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Invited)
assertThat(canBeSaved).isTrue() assertThat(canBeSaved).isTrue()
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection)) eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable)
assertThat(canBeSaved).isFalse() assertThat(canBeSaved).isFalse()
} }
} }
@@ -250,10 +250,10 @@ class SecurityAndPrivacyPresenterTest {
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
} }
with(awaitItem()) { with(awaitItem()) {
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone)) eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable)
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
} }
skipItems(1) skipItems(1)
@@ -318,10 +318,10 @@ class SecurityAndPrivacyPresenterTest {
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
} }
with(awaitItem()) { with(awaitItem()) {
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone)) eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable)
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
} }
skipItems(1) 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.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings 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.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
@@ -146,12 +147,12 @@ class SecurityAndPrivacyViewTest {
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
editedSettings = aSecurityAndPrivacySettings( editedSettings = aSecurityAndPrivacySettings(
historyVisibility = SecurityAndPrivacyHistoryVisibility.SinceSelection, historyVisibility = SecurityAndPrivacyHistoryVisibility.Invited,
), ),
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_room_history_since_selecting_option_title) rule.clickOn(R.string.screen_security_and_privacy_room_history_since_invite_option_title)
recorder.assertSingle(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection)) recorder.assertSingle(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited))
} }
@Test @Test
@@ -184,10 +185,12 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecur
state: SecurityAndPrivacyState = aSecurityAndPrivacyState( state: SecurityAndPrivacyState = aSecurityAndPrivacyState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
), ),
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
) { ) {
setContent { setContent {
SecurityAndPrivacyView( SecurityAndPrivacyView(
state = state, 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,
)
}