diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/Config.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/Config.kt new file mode 100644 index 0000000000..883e0d1dc3 --- /dev/null +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/Config.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.analytics.api + +object Config { + const val POLICY_LINK = "https://element.io/cookie-policy" +} + diff --git a/features/analytics/impl/build.gradle.kts b/features/analytics/impl/build.gradle.kts index b72d8dbbec..60b4887a88 100644 --- a/features/analytics/impl/build.gradle.kts +++ b/features/analytics/impl/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { api(projects.features.analytics.api) api(projects.services.analytics.api) implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.browser) ksp(libs.showkase.processor) testImplementation(libs.test.junit) diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt index aafb3d4490..ab060a51cf 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt @@ -16,14 +16,19 @@ package io.element.android.features.analytics.impl +import android.app.Activity +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.analytics.api.Config +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) @@ -33,12 +38,19 @@ class AnalyticsOptInNode @AssistedInject constructor( private val presenter: AnalyticsOptInPresenter, ) : Node(buildContext, plugins = plugins) { + private fun onClickTerms(activity: Activity, darkTheme: Boolean) { + activity.openUrlInChromeCustomTab(null, darkTheme, Config.POLICY_LINK) + } + @Composable override fun View(modifier: Modifier) { + val activity = LocalContext.current as Activity + val isDark = MaterialTheme.colors.isLight.not() val state = presenter.present() AnalyticsOptInView( state = state, modifier = modifier, + onClickTerms = { onClickTerms(activity, isDark) }, ) } } diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index 899c217e00..e2af75ceca 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -16,47 +16,46 @@ package io.element.android.features.analytics.impl -import android.graphics.Typeface -import android.text.SpannableString -import android.text.style.ForegroundColorSpan -import android.text.style.StyleSpan -import android.text.style.UnderlineSpan -import androidx.annotation.StringRes -import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.filled.Poll +import androidx.compose.material.icons.rounded.Check import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.analytics.api.AnalyticsOptInEvents -import io.element.android.libraries.designsystem.LinkColor +import io.element.android.libraries.designsystem.ElementTextStyles +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.pages.HeaderFooterPage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart +import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text @@ -67,157 +66,148 @@ import io.element.android.libraries.ui.strings.R as StringR @Composable fun AnalyticsOptInView( state: AnalyticsOptInState, + onClickTerms: () -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "Analytics", msg = "Root") val eventSink = state.eventSink - Box( + HeaderFooterPage( modifier = modifier .fillMaxSize() .systemBarsPadding() - .imePadding() + .imePadding(), + header = { AnalyticsOptInHeader(state, onClickTerms) }, + content = { AnalyticsOptInContent() }, + footer = { AnalyticsOptInFooter(eventSink) }) +} + +@Composable +private fun AnalyticsOptInHeader( + state: AnalyticsOptInState, + onClickTerms: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column( - modifier = Modifier.padding(horizontal = 16.dp), - ) { - Column(modifier = Modifier.weight(1f)) { - Image( - painterResource(id = R.drawable.element_logo_stars), - contentDescription = null, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(16.dp) - ) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - color = MaterialTheme.colorScheme.primary, - ) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_help_us_improve, state.applicationName), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp), - textAlign = TextAlign.Center, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.secondary, - ) - - Text( - text = buildAnnotatedStringWithColoredPart( - R.string.screen_analytics_prompt_read_terms, - R.string.screen_analytics_prompt_read_terms_content_link - ), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp), - textAlign = TextAlign.Center, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.secondary, - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(imageVector = Icons.Outlined.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_data_usage).toAnnotatedString(), - color = MaterialTheme.colorScheme.secondary, - ) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(imageVector = Icons.Outlined.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing).toAnnotatedString(), - color = MaterialTheme.colorScheme.secondary, - ) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(imageVector = Icons.Outlined.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_settings), - color = MaterialTheme.colorScheme.secondary, - ) - } - } - - Button( - onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(id = StringR.string.action_enable)) - } - Spacer(modifier = Modifier.height(16.dp)) - TextButton( - onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(id = StringR.string.action_not_now)) - } - Spacer(Modifier.height(40.dp)) - } + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), + title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName), + subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve), + iconImageVector = Icons.Filled.Poll + ) + Text( + text = buildAnnotatedStringWithStyledPart( + R.string.screen_analytics_prompt_read_terms, + R.string.screen_analytics_prompt_read_terms_content_link, + color = Color.Unspecified, + underline = false, + bold = true, + ), + modifier = Modifier + .clip(shape = RoundedCornerShape(8.dp)) + .clickable { onClickTerms() } + .padding(8.dp), + style = ElementTextStyles.Regular.subheadline, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.secondary, + ) } } -fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString { - append(this@toAnnotatedString) - val spannable = SpannableString(this@toAnnotatedString) - spannable.getSpans(0, spannable.length, Any::class.java).forEach { span -> - val start = spannable.getSpanStart(span) - val end = spannable.getSpanEnd(span) - when (span) { - is StyleSpan -> when (span.style) { - Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end) - Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end) - Typeface.BOLD_ITALIC -> addStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), start, end) - } - is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end) - is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end) +@Composable +private fun AnalyticsOptInContent( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = BiasAlignment( + horizontalBias = 0f, + verticalBias = -0.4f + ) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + AnalyticsOptInContentRow( + text = stringResource(id = R.string.screen_analytics_prompt_data_usage), + idx = 0 + ) + AnalyticsOptInContentRow( + text = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing), + idx = 1 + ) + AnalyticsOptInContentRow( + text = stringResource(id = R.string.screen_analytics_prompt_settings), + idx = 2 + ) } } } @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 - ) +private fun AnalyticsOptInContentRow( + text: String, + idx: Int, + modifier: Modifier = Modifier, +) { + val radius = 14.dp + val bgShape = when (idx) { + 0 -> RoundedCornerShape(topStart = radius, topEnd = radius) + 2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + else -> RoundedCornerShape(0.dp) + } + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = LocalColors.current.quinary, + shape = bgShape, + ) + .padding(vertical = 12.dp, horizontal = 20.dp), + ) { + Icon( + modifier = Modifier + .size(20.dp) + .background(color = MaterialTheme.colorScheme.background, shape = CircleShape) + .padding(2.dp), + imageVector = Icons.Rounded.Check, + contentDescription = null, + // TODO Compound, this color is not yet in the theme + tint = Color(0xFF007A61) + ) + Text( + modifier = Modifier.padding(start = 16.dp), + text = text, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@Composable +private fun AnalyticsOptInFooter( + eventSink: (AnalyticsOptInEvents) -> Unit, + modifier: Modifier = Modifier, +) { + ButtonColumnMolecule( + modifier = modifier, + ) { + Button( + onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(id = StringR.string.action_ok)) + } + TextButton( + onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(id = StringR.string.action_not_now)) + } + } } @Preview @@ -234,5 +224,8 @@ fun AnalyticsOptInViewDarkPreview(@PreviewParameter(AnalyticsOptInStateProvider: @Composable private fun ContentToPreview(state: AnalyticsOptInState) { - AnalyticsOptInView(state = state) + AnalyticsOptInView( + state = state, + onClickTerms = {}, + ) } diff --git a/features/analytics/impl/src/main/res/drawable/element_logo_stars.xml b/features/analytics/impl/src/main/res/drawable/element_logo_stars.xml deleted file mode 100644 index d982fbedc4..0000000000 --- a/features/analytics/impl/src/main/res/drawable/element_logo_stars.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml index b606a6d82e..e0b033f68e 100644 --- a/features/analytics/impl/src/main/res/values-de/translations.xml +++ b/features/analytics/impl/src/main/res/values-de/translations.xml @@ -1,7 +1,6 @@ "Wir erfassen und analysieren ""keine"" Account-Daten" - "Helfen Sie uns, Probleme zu identifizieren und %1$s zu verbessern, indem Sie anonyme Nutzungsdaten weitergeben." "Sie können alle unsere Nutzerbedingungen %1$s lesen." "hier" "Sie können die Analyse jederzeit in den Einstellungen deaktivieren" diff --git a/features/analytics/impl/src/main/res/values-ro/translations.xml b/features/analytics/impl/src/main/res/values-ro/translations.xml index 91beaa7c52..c46378e6da 100644 --- a/features/analytics/impl/src/main/res/values-ro/translations.xml +++ b/features/analytics/impl/src/main/res/values-ro/translations.xml @@ -1,7 +1,6 @@ "Nu"" înregistrăm sau profilăm datele contului" - "Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime." "Puteți citi toate condițiile noastre %1$s." "aici" "Puteți dezactiva această opțiune oricând din setări" diff --git a/features/analytics/impl/src/main/res/values/localazy.xml b/features/analytics/impl/src/main/res/values/localazy.xml index e6b1c6419d..d6083c860d 100644 --- a/features/analytics/impl/src/main/res/values/localazy.xml +++ b/features/analytics/impl/src/main/res/values/localazy.xml @@ -1,10 +1,10 @@ - "We ""don\'t"" record or profile any account data" - "Help us identify issues and improve %1$s by sharing anonymous usage data." + "We won\'t record or profile any personal data" + "Share anonymous usage data to help us identify issues." "You can read all our terms %1$s." "here" - "You can turn this off anytime in settings" - "We ""don\'t"" share information with third parties" + "You can turn this off anytime" + "We won\'t share your data with third parties" "Help improve %1$s" \ No newline at end of file diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index aacf4c0aeb..2236438e2a 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt index 407459c5bf..48c674e0a0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt @@ -23,6 +23,7 @@ import android.net.Uri import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.browser.customtabs.CustomTabsSession +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.ApplicationContext import javax.inject.Inject diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts index e9a8feaa05..f98914e08a 100644 --- a/libraries/androidutils/build.gradle.kts +++ b/libraries/androidutils/build.gradle.kts @@ -28,5 +28,6 @@ dependencies { implementation(libs.androidx.activity.activity) implementation(libs.androidx.exifinterface) implementation(libs.androidx.security.crypto) + implementation(libs.androidx.browser) implementation(projects.libraries.core) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt similarity index 97% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt rename to libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt index be98566e7c..ec0d9662c7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.customtab +package io.element.android.libraries.androidutils.browser import android.app.Activity import android.content.ActivityNotFoundException diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt new file mode 100644 index 0000000000..7d4de84301 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.text + +import android.graphics.Typeface +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.text.style.UnderlineSpan +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import io.element.android.libraries.designsystem.LinkColor + +fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString { + append(this@toAnnotatedString) + val spannable = SpannableString(this@toAnnotatedString) + spannable.getSpans(0, spannable.length, Any::class.java).forEach { span -> + val start = spannable.getSpanStart(span) + val end = spannable.getSpanEnd(span) + when (span) { + is StyleSpan -> when (span.style) { + Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end) + Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end) + Typeface.BOLD_ITALIC -> addStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), start, end) + } + is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end) + is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end) + } + } +} + +/** + * Convert a string to an [AnnotatedString] with styles applied. + * + * @param fullTextRes the string resource to use as the full text. Must contain a single %s + * @param coloredTextRes the string resource to use as the colored part of the string + * @param color the color to apply to the string + * @param underline whether to underline the string + * @param bold whether to bold the string + */ +@Composable +fun buildAnnotatedStringWithStyledPart( + @StringRes fullTextRes: Int, + @StringRes coloredTextRes: Int, + color: Color = LinkColor, + underline: Boolean = true, + bold: Boolean = false, +) = 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, + fontWeight = if (bold) FontWeight.Bold else null, + ), + start = startIndex, + end = startIndex + coloredPart.length, + ) +} diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index eaaa4b3a91..23bd49cb88 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88d5d2054bae36664b69a20853e11e4567bd6cb1e6a8f7ea0a6747ec534807f6 -size 47160 +oid sha256:bde1e07ccd740f6f19bc39be2dc0feaff956a6fbf079639b3f0949b7f2d2dc96 +size 51661 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index cf5be1e386..d44e6c6dce 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9774baeb4de35b8fc4f47d4e41eb38f856a098465ffce0cdea66c32e4cebf57 -size 46475 +oid sha256:b5c99a23745883c9b6f9c5112974e9e80f202d7a5255776f66c638af4ed28abc +size 51339