Improve in-app update release notes formatting
This commit is contained in:
parent
a503091ab2
commit
5f5d4cde98
|
@ -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 =
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue