Merge pull request #1177 from vector-im/feature/bma/analyticsSettingLink

Fix issue on analytics setting link
This commit is contained in:
Benoit Marty 2023-08-30 11:16:46 +02:00 committed by GitHub
commit e67c7ac9f1
15 changed files with 125 additions and 99 deletions

1
changelog.d/1177.bugfix Normal file
View file

@ -0,0 +1 @@
Add missing link to the terms on the analytics setting screen.

View file

@ -21,5 +21,6 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents
data class AnalyticsPreferencesState( data class AnalyticsPreferencesState(
val applicationName: String, val applicationName: String,
val isEnabled: Boolean, val isEnabled: Boolean,
val policyUrl: String,
val eventSink: (AnalyticsOptInEvents) -> Unit, val eventSink: (AnalyticsOptInEvents) -> Unit,
) )

View file

@ -28,5 +28,6 @@ open class AnalyticsPreferencesStateProvider : PreviewParameterProvider<Analytic
fun aAnalyticsPreferencesState() = AnalyticsPreferencesState( fun aAnalyticsPreferencesState() = AnalyticsPreferencesState(
applicationName = "Element X", applicationName = "Element X",
isEnabled = false, isEnabled = false,
policyUrl = "https://element.io",
eventSink = {} eventSink = {}
) )

View file

@ -16,22 +16,21 @@
package io.element.android.features.analytics.api.preferences package io.element.android.features.analytics.api.preferences
import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.LINK_TAG
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.theme.LinkColor import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@ -43,40 +42,33 @@ fun AnalyticsPreferencesView(
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled)) state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
} }
val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName) val supportingText = stringResource(
val secondPart = buildAnnotatedStringWithColoredPart( id = CommonStrings.screen_analytics_settings_help_us_improve,
state.applicationName
)
val linkText = buildAnnotatedStringWithStyledPart(
CommonStrings.screen_analytics_settings_read_terms, CommonStrings.screen_analytics_settings_read_terms,
CommonStrings.screen_analytics_settings_read_terms_content_link CommonStrings.screen_analytics_settings_read_terms_content_link,
) tagAndLink = LINK_TAG to state.policyUrl,
val subtitle = "$firstPart\n\n$secondPart"
PreferenceSwitch(
modifier = modifier,
title = stringResource(id = CommonStrings.screen_analytics_settings_share_data),
subtitle = subtitle,
isChecked = state.isEnabled,
onCheckedChange = ::onEnabledChanged,
switchAlignment = Alignment.Top,
)
}
@Composable
fun buildAnnotatedStringWithColoredPart(
@StringRes fullTextRes: Int,
@StringRes coloredTextRes: Int,
color: Color = LinkColor,
underline: Boolean = true,
) = buildAnnotatedString {
val coloredPart = stringResource(coloredTextRes)
val fullText = stringResource(fullTextRes, coloredPart)
val startIndex = fullText.indexOf(coloredPart)
append(fullText)
addStyle(
style = SpanStyle(
color = color,
textDecoration = if (underline) TextDecoration.Underline else null
), start = startIndex, end = startIndex + coloredPart.length
) )
Column(modifier) {
ListItem(
headlineContent = {
Text(stringResource(id = CommonStrings.screen_analytics_settings_share_data))
},
supportingContent = {
Text(supportingText)
},
leadingContent = null,
trailingContent = ListItemContent.Switch(
checked = state.isEnabled,
),
onClick = {
onEnabledChanged(!state.isEnabled)
}
)
ListSupportingText(annotatedString = linkText)
}
} }
@Preview @Preview
@ -91,5 +83,7 @@ internal fun AnalyticsPreferencesViewDarkPreview(@PreviewParameter(AnalyticsPref
@Composable @Composable
private fun ContentToPreview(state: AnalyticsPreferencesState) { private fun ContentToPreview(state: AnalyticsPreferencesState) {
AnalyticsPreferencesView(state) AnalyticsPreferencesView(
state = state,
)
} }

View file

@ -18,7 +18,6 @@ package io.element.android.features.analytics.impl
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -28,7 +27,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Poll import androidx.compose.material.icons.filled.Poll
import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Check
@ -37,7 +36,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.BiasAlignment import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -45,6 +43,7 @@ import androidx.compose.ui.tooling.preview.Preview
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.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.Config
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem
@ -56,7 +55,6 @@ import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithSt
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
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.temporaryColorBgSpecial import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.LogCompositions
@ -98,6 +96,8 @@ fun AnalyticsOptInView(
) )
} }
private const val LINK_TAG = "link"
@Composable @Composable
private fun AnalyticsOptInHeader( private fun AnalyticsOptInHeader(
state: AnalyticsOptInState, state: AnalyticsOptInState,
@ -114,21 +114,29 @@ private fun AnalyticsOptInHeader(
subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve), subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
iconImageVector = Icons.Filled.Poll iconImageVector = Icons.Filled.Poll
) )
Text( val text = buildAnnotatedStringWithStyledPart(
text = buildAnnotatedStringWithStyledPart( R.string.screen_analytics_prompt_read_terms,
R.string.screen_analytics_prompt_read_terms, R.string.screen_analytics_prompt_read_terms_content_link,
R.string.screen_analytics_prompt_read_terms_content_link, color = Color.Unspecified,
color = Color.Unspecified, underline = false,
underline = false, bold = true,
bold = true, tagAndLink = LINK_TAG to Config.POLICY_LINK,
), )
ClickableText(
text = text,
onClick = {
text
.getStringAnnotations(LINK_TAG, it, it)
.firstOrNull()
?.let { _ -> onClickTerms() }
},
modifier = Modifier modifier = Modifier
.clip(shape = RoundedCornerShape(8.dp))
.clickable { onClickTerms() }
.padding(8.dp), .padding(8.dp),
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular
textAlign = TextAlign.Center, .copy(
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.Center,
)
) )
} }
} }

View file

@ -21,6 +21,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.Config
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
@ -51,6 +52,7 @@ class DefaultAnalyticsPreferencesPresenter @Inject constructor(
return AnalyticsPreferencesState( return AnalyticsPreferencesState(
applicationName = buildMeta.applicationName, applicationName = buildMeta.applicationName,
isEnabled = isEnabled.value, isEnabled = isEnabled.value,
policyUrl = Config.POLICY_LINK,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }

View file

@ -39,6 +39,7 @@ class AnalyticsPreferencesPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isEnabled).isTrue() assertThat(initialState.isEnabled).isTrue()
assertThat(initialState.policyUrl).isNotEmpty()
} }
} }

View file

@ -21,7 +21,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -35,6 +37,7 @@ import io.element.android.libraries.designsystem.components.ClickableLinkText
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.text.toAnnotatedString import io.element.android.libraries.designsystem.text.toAnnotatedString
import io.element.android.libraries.theme.ElementTheme
@Composable @Composable
fun TimelineItemTextView( fun TimelineItemTextView(
@ -45,31 +48,33 @@ fun TimelineItemTextView(
onTextClicked: () -> Unit = {}, onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {}, onTextLongClicked: () -> Unit = {},
) { ) {
val htmlDocument = content.htmlDocument CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textPrimary) {
if (htmlDocument != null) { val htmlDocument = content.htmlDocument
// For now we ignore the extra padding for html content, so add some spacing if (htmlDocument != null) {
// below the content (as previous behavior) // For now we ignore the extra padding for html content, so add some spacing
Column(modifier = modifier) { // below the content (as previous behavior)
HtmlDocument( Column(modifier = modifier) {
document = htmlDocument, HtmlDocument(
modifier = Modifier, document = htmlDocument,
onTextClicked = onTextClicked, modifier = Modifier,
onTextLongClicked = onTextLongClicked, onTextClicked = onTextClicked,
interactionSource = interactionSource onTextLongClicked = onTextLongClicked,
) interactionSource = interactionSource
Spacer(Modifier.height(16.dp)) )
} Spacer(Modifier.height(16.dp))
} else { }
Box(modifier) { } else {
val textWithPadding = remember(content.body) { Box(modifier) {
content.body + extraPadding.getStr(16.sp).toAnnotatedString() val textWithPadding = remember(content.body) {
content.body + extraPadding.getStr(16.sp).toAnnotatedString()
}
ClickableLinkText(
text = textWithPadding,
onClick = onTextClicked,
onLongClick = onTextLongClicked,
interactionSource = interactionSource
)
} }
ClickableLinkText(
text = textWithPadding,
onClick = onTextClicked,
onLongClick = onTextLongClicked,
interactionSource = interactionSource
)
} }
} }
} }

View file

@ -24,7 +24,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -79,8 +78,8 @@ fun ClickableLinkText(
@Composable @Composable
fun ClickableLinkText( fun ClickableLinkText(
annotatedString: AnnotatedString, annotatedString: AnnotatedString,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
linkify: Boolean = true, linkify: Boolean = true,
linkAnnotationTag: String = LINK_TAG, linkAnnotationTag: String = LINK_TAG,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
@ -136,7 +135,6 @@ fun ClickableLinkText(
layoutResult.value = it layoutResult.value = it
}, },
inlineContent = inlineContent, inlineContent = inlineContent,
color = MaterialTheme.colorScheme.primary,
) )
} }

View file

@ -59,6 +59,7 @@ fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
* @param color the color to apply to the string * @param color the color to apply to the string
* @param underline whether to underline the string * @param underline whether to underline the string
* @param bold whether to bold the string * @param bold whether to bold the string
* @param tagAndLink an optional pair of tag and link to add to the styled part of the string, as StringAnnotation
*/ */
@Composable @Composable
fun buildAnnotatedStringWithStyledPart( fun buildAnnotatedStringWithStyledPart(
@ -67,6 +68,7 @@ fun buildAnnotatedStringWithStyledPart(
color: Color = LinkColor, color: Color = LinkColor,
underline: Boolean = true, underline: Boolean = true,
bold: Boolean = false, bold: Boolean = false,
tagAndLink: Pair<String, String>? = null,
) = buildAnnotatedString { ) = buildAnnotatedString {
val coloredPart = stringResource(coloredTextRes) val coloredPart = stringResource(coloredTextRes)
val fullText = stringResource(fullTextRes, coloredPart) val fullText = stringResource(fullTextRes, coloredPart)
@ -81,6 +83,14 @@ fun buildAnnotatedStringWithStyledPart(
start = startIndex, start = startIndex,
end = startIndex + coloredPart.length, end = startIndex + coloredPart.length,
) )
if (tagAndLink != null) {
addStringAnnotation(
tag = tagAndLink.first,
annotation = tagAndLink.second,
start = startIndex,
end = startIndex + coloredPart.length
)
}
} }
/** /**

View file

@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Share import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
@ -30,9 +29,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.preview.PreviewGroup
@ -103,17 +104,21 @@ fun ListSupportingText(
* @param modifier The modifier to be applied to the text. * @param modifier The modifier to be applied to the text.
* @param contentPadding The padding to apply to the text. Default is [ListSupportingTextDefaults.Padding.Default]. * @param contentPadding The padding to apply to the text. Default is [ListSupportingTextDefaults.Padding.Default].
*/ */
@OptIn(ExperimentalTextApi::class)
@Composable @Composable
fun ListSupportingText( fun ListSupportingText(
annotatedString: AnnotatedString, annotatedString: AnnotatedString,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentPadding: ListSupportingTextDefaults.Padding = ListSupportingTextDefaults.Padding.Default, contentPadding: ListSupportingTextDefaults.Padding = ListSupportingTextDefaults.Padding.Default,
) { ) {
Text( val style = ElementTheme.typography.fontBodySmRegular
text = annotatedString, .copy(color = ElementTheme.colors.textSecondary)
modifier = modifier.padding(contentPadding.paddingValues()), val paddedModifier = modifier.padding(contentPadding.paddingValues())
style = ElementTheme.typography.fontBodySmRegular, ClickableLinkText(
color = ElementTheme.colors.textSecondary, annotatedString = annotatedString,
modifier = paddedModifier,
style = style,
linkify = false,
) )
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a08d5ff1f91c0db7c502882815d36d6a675567ebf8c3eddc0ebff431e3592e67 oid sha256:b365229cac3351e4ec44979b7a22fcae090a8995e4784d9114cfd0033a242510
size 23390 size 23147

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6a5300ac57d9d0137cf82af4ed4bd86c3ab7f3a70d2560954e524b7b3c120199 oid sha256:5fc43706603ce52fa3495fa4e6dfa9aa684540a9dec996a5f705427d34ffb55c
size 23515 size 23274

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4dd40c1eac2e5f33ce340ce69d8c0f502cd0912a0d511602e1122a6f4d8ae330 oid sha256:e22dc131e8f1f7461c050e871eaa408895529976ef7445aa6faf09852370df90
size 25433 size 25176

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:76dbcb5845565774bd33a192d02a3da0216c34b17e6e1bc1f208cabef9506617 oid sha256:bc234611dd3df74196467129476c69a0212a4c665f2a94797c175cbd911c2083
size 26630 size 26339