committed by
Benoit Marty
parent
98f43f2402
commit
d04ebe880b
@@ -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) },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user