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