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
private const val RELEASE_NOTES_MAX_LENGTH = 500
private const val NEW_LINE = "\n"
data class ChangelogEntry(
val version: String,
val date: String,
@ -27,9 +30,17 @@ data class ChangelogEntry(
}
private fun StringBuilder.appendChangeLogSection(section: ChangelogEntrySection) {
appendLine(section.title)
appendLine(section.content)
appendLine()
appendIfCan(section.title)
appendIfCan(section.content)
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 =

View File

@ -10,7 +10,7 @@ import java.util.Locale
object ChangelogParser {
// 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 UNRELEASED_TITLE_POSITION = 4
@ -23,7 +23,8 @@ object ChangelogParser {
fun getChangelogEntry(
filePath: String,
versionNameFallback: String
versionNameFallback: String,
keywords: LocalizedKeywords,
): ChangelogEntry {
log("Parser: starting...")
@ -40,13 +41,13 @@ object ChangelogParser {
// Validate content
check(
nodes[CHANGELOG_TITLE_POSITION].contains("# Changelog") &&
nodes[UNRELEASED_TITLE_POSITION].contains("## [Unreleased]")
nodes[CHANGELOG_TITLE_POSITION].contains("# ${keywords.changelog}") &&
nodes[UNRELEASED_TITLE_POSITION].contains("## [${keywords.unreleased}]")
) {
"Provided changelog file is incorrect or its structure is malformed."
}
val fromIndex = findFirstValidNodeIndex(nodes)
val fromIndex = findFirstValidNodeIndex(nodes, keywords)
log("Parser: index from: $fromIndex")
val toIndex =
@ -65,12 +66,12 @@ object ChangelogParser {
val lastChangelogEntry =
nodes.subList(fromIndex = fromIndex, toIndex = toIndex).let { parts ->
ChangelogEntry(
version = parts.getVersionPart(versionNameFallback),
date = parts.getDatePart(),
added = parts.getNodePart("Added"),
changed = parts.getNodePart("Changed"),
fixed = parts.getNodePart("Fixed"),
removed = parts.getNodePart("Removed"),
version = parts.getVersionPart(versionNameFallback, keywords),
date = parts.getDatePart(keywords),
added = parts.getNodePart(keywords.added),
changed = parts.getNodePart(keywords.changed),
fixed = parts.getNodePart(keywords.fixed),
removed = parts.getNodePart(keywords.removed),
)
}
@ -78,9 +79,9 @@ object ChangelogParser {
return lastChangelogEntry
}
private fun findFirstValidNodeIndex(nodes: List<String>): Int {
private fun findFirstValidNodeIndex(nodes: List<String>, keywords: LocalizedKeywords): Int {
nodes.forEachIndexed { index, node ->
if (findNodeByPrefix(node) && findValidSubNodeByPrefix(nodes[index + 1])) {
if (findNodeByPrefix(node) && findValidSubNodeByPrefix(nodes[index + 1], keywords)) {
return index
}
}
@ -90,25 +91,25 @@ object ChangelogParser {
private fun findNodeByPrefix(node: String): Boolean = node.startsWith("## [")
private fun findValidSubNodeByPrefix(subNode: String): Boolean =
subNode.startsWith("### Added") ||
subNode.startsWith("### Changed") ||
subNode.startsWith("### Fixed") ||
subNode.startsWith("### Removed")
private fun findValidSubNodeByPrefix(subNode: String, keywords: LocalizedKeywords): Boolean =
subNode.startsWith("### ${keywords.added}") ||
subNode.startsWith("### ${keywords.changed}") ||
subNode.startsWith("### ${keywords.fixed}") ||
subNode.startsWith("### ${keywords.removed}")
private fun List<String>.getVersionPart(versionNameFallback: String): String {
return if (this.contains("## [Unreleased]")) {
private fun List<String>.getVersionPart(versionNameFallback: String, keywords: LocalizedKeywords): String {
return if (this.contains("## [${keywords.unreleased}]")) {
versionNameFallback
} else {
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 {
return if (this.contains("## [Unreleased]")) {
dateFormatter.format(Date())
private fun List<String>.getDatePart(keywords: LocalizedKeywords): String {
return if (this.contains("## [${keywords.unreleased}]")) {
fallbackDateFormatter.format(Date())
} else {
this[0].split("- ")[1].trim()
}
@ -134,9 +135,19 @@ object ChangelogParser {
}
}
return subList(startIndex, endIndex)
.joinToString(prefix = "\n", separator = "\n")
.map { it.replace("\n- ", "\n") }
.joinToString(separator = "\n")
.takeIf { it.isNotBlank() }?.let {
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()
text = ChangelogParser.getChangelogEntry(
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()
}
val releaseNotes: MutableList<LocalizedText> = arrayListOf(localizedText)

View File

@ -26,7 +26,15 @@ val generateBuildConfigTask = tasks.create("buildConfig") {
val releaseNotesJson = ChangelogParser.getChangelogEntry(
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()
inputs.property("gitSha", gitInfo.sha)

View File

@ -20,3 +20,6 @@ directly impact users rather than highlighting other key architectural updates.*
### Fixed
- 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
### 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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
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.TextIndent
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.R
@ -50,15 +42,15 @@ fun WhatsNewView(
) { paddingValues ->
Column(
modifier =
Modifier
.fillMaxSize()
.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault,
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
.verticalScroll(rememberScrollState())
Modifier
.fillMaxSize()
.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault,
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
.verticalScroll(rememberScrollState())
) {
Row {
Text(
@ -87,43 +79,17 @@ fun WhatsNewView(
@Composable
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 {
Text(
text = state.title.getValue(),
text = stringResource(id = R.string.whats_new_entry_title, state.title.getValue()),
style = ZcashTheme.typography.primary.titleSmall,
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingMin))
Text(
text = bulletStyle,
style = bulletTextStyle
text = state.content.getValue(),
style = ZcashTheme.typography.primary.bodySmall
)
}
}

View File

@ -2,4 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="whats_new_title">What\'s new</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>