Improve in-app update release notes formatting

This commit is contained in:
Honza 2024-07-20 22:45:30 +02:00
parent a503091ab2
commit 5f5d4cde98
7 changed files with 84 additions and 76 deletions

View File

@ -2,6 +2,9 @@ package publish
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
private const val RELEASE_NOTES_MAX_LENGTH = 500
private const val NEW_LINE = "\n"
data class ChangelogEntry( data class ChangelogEntry(
val version: String, val version: String,
val date: String, val date: String,
@ -27,9 +30,17 @@ data class ChangelogEntry(
} }
private fun StringBuilder.appendChangeLogSection(section: ChangelogEntrySection) { private fun StringBuilder.appendChangeLogSection(section: ChangelogEntrySection) {
appendLine(section.title) appendIfCan(section.title)
appendLine(section.content) appendIfCan(section.content)
appendLine() appendIfCan(NEW_LINE)
}
private fun StringBuilder.appendIfCan(content: String) {
if (length + content.length <= RELEASE_NOTES_MAX_LENGTH) {
append(content)
} else {
println("WARN: Some in-app update release notes have been skipped: $content")
}
} }
fun toJsonString(): String = fun toJsonString(): String =

View File

@ -10,7 +10,7 @@ import java.util.Locale
object ChangelogParser { object ChangelogParser {
// Enable this when you need detailed parser logging. This should be turned off for production builds. // Enable this when you need detailed parser logging. This should be turned off for production builds.
private const val DEBUG_LOGS_ENABLED = false private const val DEBUG_LOGS_ENABLED = true
private const val CHANGELOG_TITLE_POSITION = 0 private const val CHANGELOG_TITLE_POSITION = 0
private const val UNRELEASED_TITLE_POSITION = 4 private const val UNRELEASED_TITLE_POSITION = 4
@ -23,7 +23,8 @@ object ChangelogParser {
fun getChangelogEntry( fun getChangelogEntry(
filePath: String, filePath: String,
versionNameFallback: String versionNameFallback: String,
keywords: LocalizedKeywords,
): ChangelogEntry { ): ChangelogEntry {
log("Parser: starting...") log("Parser: starting...")
@ -40,13 +41,13 @@ object ChangelogParser {
// Validate content // Validate content
check( check(
nodes[CHANGELOG_TITLE_POSITION].contains("# Changelog") && nodes[CHANGELOG_TITLE_POSITION].contains("# ${keywords.changelog}") &&
nodes[UNRELEASED_TITLE_POSITION].contains("## [Unreleased]") nodes[UNRELEASED_TITLE_POSITION].contains("## [${keywords.unreleased}]")
) { ) {
"Provided changelog file is incorrect or its structure is malformed." "Provided changelog file is incorrect or its structure is malformed."
} }
val fromIndex = findFirstValidNodeIndex(nodes) val fromIndex = findFirstValidNodeIndex(nodes, keywords)
log("Parser: index from: $fromIndex") log("Parser: index from: $fromIndex")
val toIndex = val toIndex =
@ -65,12 +66,12 @@ object ChangelogParser {
val lastChangelogEntry = val lastChangelogEntry =
nodes.subList(fromIndex = fromIndex, toIndex = toIndex).let { parts -> nodes.subList(fromIndex = fromIndex, toIndex = toIndex).let { parts ->
ChangelogEntry( ChangelogEntry(
version = parts.getVersionPart(versionNameFallback), version = parts.getVersionPart(versionNameFallback, keywords),
date = parts.getDatePart(), date = parts.getDatePart(keywords),
added = parts.getNodePart("Added"), added = parts.getNodePart(keywords.added),
changed = parts.getNodePart("Changed"), changed = parts.getNodePart(keywords.changed),
fixed = parts.getNodePart("Fixed"), fixed = parts.getNodePart(keywords.fixed),
removed = parts.getNodePart("Removed"), removed = parts.getNodePart(keywords.removed),
) )
} }
@ -78,9 +79,9 @@ object ChangelogParser {
return lastChangelogEntry return lastChangelogEntry
} }
private fun findFirstValidNodeIndex(nodes: List<String>): Int { private fun findFirstValidNodeIndex(nodes: List<String>, keywords: LocalizedKeywords): Int {
nodes.forEachIndexed { index, node -> nodes.forEachIndexed { index, node ->
if (findNodeByPrefix(node) && findValidSubNodeByPrefix(nodes[index + 1])) { if (findNodeByPrefix(node) && findValidSubNodeByPrefix(nodes[index + 1], keywords)) {
return index return index
} }
} }
@ -90,25 +91,25 @@ object ChangelogParser {
private fun findNodeByPrefix(node: String): Boolean = node.startsWith("## [") private fun findNodeByPrefix(node: String): Boolean = node.startsWith("## [")
private fun findValidSubNodeByPrefix(subNode: String): Boolean = private fun findValidSubNodeByPrefix(subNode: String, keywords: LocalizedKeywords): Boolean =
subNode.startsWith("### Added") || subNode.startsWith("### ${keywords.added}") ||
subNode.startsWith("### Changed") || subNode.startsWith("### ${keywords.changed}") ||
subNode.startsWith("### Fixed") || subNode.startsWith("### ${keywords.fixed}") ||
subNode.startsWith("### Removed") subNode.startsWith("### ${keywords.removed}")
private fun List<String>.getVersionPart(versionNameFallback: String): String { private fun List<String>.getVersionPart(versionNameFallback: String, keywords: LocalizedKeywords): String {
return if (this.contains("## [Unreleased]")) { return if (this.contains("## [${keywords.unreleased}]")) {
versionNameFallback versionNameFallback
} else { } else {
this[0].split("[")[1].split("]")[0].trim() this[0].split("[")[1].split("]")[0].trim()
} }
} }
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) private val fallbackDateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
private fun List<String>.getDatePart(): String { private fun List<String>.getDatePart(keywords: LocalizedKeywords): String {
return if (this.contains("## [Unreleased]")) { return if (this.contains("## [${keywords.unreleased}]")) {
dateFormatter.format(Date()) fallbackDateFormatter.format(Date())
} else { } else {
this[0].split("- ")[1].trim() this[0].split("- ")[1].trim()
} }
@ -134,9 +135,19 @@ object ChangelogParser {
} }
} }
return subList(startIndex, endIndex) return subList(startIndex, endIndex)
.joinToString(prefix = "\n", separator = "\n") .map { it.replace("\n- ", "\n") }
.joinToString(separator = "\n")
.takeIf { it.isNotBlank() }?.let { .takeIf { it.isNotBlank() }?.let {
ChangelogEntrySection(title = title, content = it) ChangelogEntrySection(title = title, content = it)
} }
} }
data class LocalizedKeywords(
val changelog: String,
val unreleased: String,
val added: String,
val changed: String,
val fixed: String,
val removed: String,
)
} }

View File

@ -246,7 +246,15 @@ abstract class PublishToGooglePlay @Inject constructor(
language = Locale.ENGLISH.toLanguageTag() language = Locale.ENGLISH.toLanguageTag()
text = ChangelogParser.getChangelogEntry( text = ChangelogParser.getChangelogEntry(
filePath = "docs/whatsNew/WHATS_NEW_EN.md", filePath = "docs/whatsNew/WHATS_NEW_EN.md",
versionNameFallback = gradleVersionName versionNameFallback = gradleVersionName,
keywords = ChangelogParser.LocalizedKeywords(
changelog = "Changelog",
unreleased = "Unreleased",
added = "Added",
changed = "Changed",
fixed = "Fixed",
removed = "Removed",
)
).toInAppUpdateReleaseNotesText() ).toInAppUpdateReleaseNotesText()
} }
val releaseNotes: MutableList<LocalizedText> = arrayListOf(localizedText) val releaseNotes: MutableList<LocalizedText> = arrayListOf(localizedText)

View File

@ -26,7 +26,15 @@ val generateBuildConfigTask = tasks.create("buildConfig") {
val releaseNotesJson = ChangelogParser.getChangelogEntry( val releaseNotesJson = ChangelogParser.getChangelogEntry(
filePath = "docs/whatsNew/WHATS_NEW_EN.md", filePath = "docs/whatsNew/WHATS_NEW_EN.md",
versionNameFallback = gradleVersionName versionNameFallback = gradleVersionName,
keywords = ChangelogParser.LocalizedKeywords(
changelog = "Changelog",
unreleased = "Unreleased",
added = "Added",
changed = "Changed",
fixed = "Fixed",
removed = "Removed",
)
).toJsonString() ).toJsonString()
inputs.property("gitSha", gitInfo.sha) inputs.property("gitSha", gitInfo.sha)

View File

@ -20,3 +20,6 @@ directly impact users rather than highlighting other key architectural updates.*
### Fixed ### Fixed
- Support Screen now shows the Send button above keyboard instead of overlaying it - Support Screen now shows the Send button above keyboard instead of overlaying it
- QR code scanning speed and reliability have been improved to address the latest reported scan issue - QR code scanning speed and reliability have been improved to address the latest reported scan issue
### Removed
- Test - test - test fine!

View File

@ -8,19 +8,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
@ -50,15 +42,15 @@ fun WhatsNewView(
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding( .padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault, top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault, bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault,
start = ZcashTheme.dimens.screenHorizontalSpacingRegular, start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular end = ZcashTheme.dimens.screenHorizontalSpacingRegular
) )
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
Row { Row {
Text( Text(
@ -87,43 +79,17 @@ fun WhatsNewView(
@Composable @Composable
private fun WhatsNewSection(state: WhatsNewSectionState) { private fun WhatsNewSection(state: WhatsNewSectionState) {
val bulletString = "\u2022\t\t"
val bulletTextStyle = MaterialTheme.typography.bodySmall
val bulletTextMeasurer = rememberTextMeasurer()
val bulletStringWidth =
remember(bulletTextStyle, bulletTextMeasurer) {
bulletTextMeasurer.measure(text = bulletString, style = bulletTextStyle).size.width
}
val bulletRestLine = with(LocalDensity.current) { bulletStringWidth.toSp() }
val bulletParagraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = bulletRestLine))
val bulletStyle =
state.content.getValue().split("\n-")
.filter { it.isNotBlank() }
.map {
it.replace("\n-", "").trim()
}
.let { text ->
buildAnnotatedString {
text.forEach {
withStyle(style = bulletParagraphStyle) {
append(bulletString)
append(it)
}
}
}
}
Column { Column {
Text( Text(
text = state.title.getValue(), text = stringResource(id = R.string.whats_new_entry_title, state.title.getValue()),
style = ZcashTheme.typography.primary.titleSmall, style = ZcashTheme.typography.primary.titleSmall,
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingMin)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingMin))
Text( Text(
text = bulletStyle, text = state.content.getValue(),
style = bulletTextStyle style = ZcashTheme.typography.primary.bodySmall
) )
} }
} }

View File

@ -2,4 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="whats_new_title">What\'s new</string> <string name="whats_new_title">What\'s new</string>
<string name="whats_new_version">Zashi Version %s</string> <string name="whats_new_version">Zashi Version %s</string>
<string name="whats_new_entry_title"><xliff:g id="entry_title" example="Added">%1$s</xliff:g>:</string>
</resources> </resources>