Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
Expand All @@ -32,13 +33,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.kickstarter.features.projectstory.data.RichTextItem
import com.kickstarter.features.projectstory.ui.RichTextItemPhotoComponent
import com.kickstarter.features.projectstory.ui.RichTextItemTextComponent
import com.kickstarter.features.projectstory.ui.WebViewComponent
import com.kickstarter.libs.KSString
import com.kickstarter.libs.utils.ApplicationUtils
import com.kickstarter.libs.utils.extensions.getEnvironment
import com.kickstarter.libs.utils.extensions.isDarkModeEnabled
import com.kickstarter.ui.compose.ProjectImageFromURl
Expand All @@ -63,12 +67,22 @@ class ProjectStoryActivity : ComponentActivity() {
projectStoryViewModelFactory = ProjectStoryViewModel.Factory(env)

setContent {
val context = LocalContext.current

val uriHandler = object : UriHandler {
override fun openUri(uri: String) {
ApplicationUtils.openUrlExternally(context, uri)
}
}

KickstarterApp(
useDarkTheme = isDarkModeEnabled(env)
) {
val uiState = projectStoryViewModel.projectStoryUiState.collectAsState()
val txtState = projectStoryViewModel.txt
CampaignScreen(uiState, txtState)
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
CampaignScreen(uiState, txtState)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.LocalPinnableContainer
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Bullet
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.TextStyle
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 androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
Expand All @@ -31,8 +35,8 @@ import com.kickstarter.features.projectstory.data.RichTextItem
import com.kickstarter.libs.utils.Secrets
import com.kickstarter.libs.utils.extensions.getEnvironment
import com.kickstarter.ui.compose.designsystem.grey_03
import com.kickstarter.ui.compose.designsystem.kds_create_700
import timber.log.Timber
import kotlin.collections.forEach

object StoryTheme {
object Typography {
Expand All @@ -42,6 +46,10 @@ object StoryTheme {
val heading3 = TextStyle.Default.merge(fontSize = 24.sp)
val heading4 = TextStyle.Default.merge(fontSize = 22.sp)
}

object InlineStyles {
val link = SpanStyle(color = kds_create_700, textDecoration = TextDecoration.Underline)
}
}

@Composable
Expand All @@ -56,7 +64,8 @@ fun RichTextItemTextComponent(item: RichTextItem.Text) {
is RichTextItem.Text.ChildParagraph -> null
}

/* Should always be empty for first-level items */
/* Currently, `styles` is empty for all first-level items except `Header`.
* At this point, the GQL transformer has already used `styles` to determine `Header.level` */
val styles =
when (item) {
is RichTextItem.Text.Paragraph -> item.styles
Expand Down Expand Up @@ -120,21 +129,52 @@ fun RichTextItemTextComponent(item: RichTextItem.Text) {
}

private fun parseRichTextChildrenOfRichText(children: List<RichTextItem.Text.ChildParagraph>): AnnotatedString {
/* Look for ways to optimize */
return buildAnnotatedString {
children.forEach {
Timber.d("")
if (it.styles.isNullOrEmpty()) {
append("${it.text} ")
} else {
// append(" ")
withStyle(
SpanStyle(
fontWeight = if (it.styles.contains("STRONG")) FontWeight.Bold else null,
fontStyle = if (it.styles.contains("EMPHASIS")) FontStyle.Italic else FontStyle.Normal
children.forEachIndexed { index, it ->
val text = it.text ?: ""

val style = it.styles?.let { styles ->
SpanStyle(
fontWeight = if (styles.contains("STRONG")) FontWeight.Bold else null,
fontStyle = if (styles.contains("EMPHASIS")) FontStyle.Italic else FontStyle.Normal
)
}

val linkAnnotation = it.link?.let {
/* Properties from the design system `link` SpanStyle will override any competing properties
* in the `style` determined by the server response. As of 2026-03-03 there are none. */
val linkBaseStyle = StoryTheme.InlineStyles.link.let { default ->
style?.merge(default) ?: default
}
LinkAnnotation.Url(
it,
styles = TextLinkStyles(
style = linkBaseStyle
)
) {
append("${it.text}")
)
}

/* Join all sibling text with a space _except_ if the text starts with certain
* kinds of punctuation. This is to handle a peculiarity of how the server-side parser
* deals w/ spaces, and is likely to be changed on the server-side in the near future. */
val firstCharacter = text.firstOrNull()
if (index != 0 && firstCharacter.needsLeadingSpace()) {
append(" ")
}

when {
linkAnnotation != null -> {
withLink(linkAnnotation) {
append(text)
}
}
style != null -> {
withStyle(style) {
append(text)
}
}
else -> {
append(text)
}
}
}
Expand Down Expand Up @@ -190,3 +230,12 @@ fun WebViewComponent(url: String) {
}
)
}

private fun Char?.needsLeadingSpace(): Boolean {
if (this == null || isWhitespace()) return false

val type = Character.getType(this)
return type != Character.END_PUNCTUATION.toInt() &&
type != Character.FINAL_QUOTE_PUNCTUATION.toInt() &&
this !in ",.!?:;"
}