One tag-name fold on the web and on Android, beside the core's (#5373, #5385)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 11s
Android / Core and FFI clippy and tests (push) Successful in 1m0s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m16s
CI & Build / integration (push) Successful in 2m41s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m8s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m45s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 9m26s
Android / Build the server image (push) Successful in 1s

The web compared or keyed tag names with name.toLowerCase() at five sites:
the labels store's merge check, LabelPicker's create check, NoteCard's
colour map, and colors.ts twice. Android had three, plus TagsScreen's rename
dialog using equals(ignoreCase = true). Each now calls foldTag(name), which
says what the core's derive::fold and the server's labeling.named say.

Android's rename dialog now predicts a merge the same way the core decides
one. Per-character ignoreCase and full lowercasing disagree on letters like
İ, whose lowercase is two characters. LabelPicker's search filter keeps
its own lowercasing: that is search, not identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 17:05:30 -04:00
co-authored by Claude Opus 5.5
parent 0ecff7eeb9
commit d6204d320a
8 changed files with 36 additions and 18 deletions
@@ -86,9 +86,9 @@ fun derivedTint(id: String): String {
* Deriving has no mint points to miss and no backfill for the tags already out there.
* The cost is that renaming a tag recolours it, which is fair: the name IS the tag.
*
* Lowercased because tags dedupe case-insensitively — `#Todo` renamed to `#todo` is
* the same tag and should not change colour. Kotlin's `lowercase()` and the web's
* `toLowerCase()` are both locale-independent, so the mirror holds.
* Folded because `#Todo` renamed to `#todo` is the same tag and should not change
* colour. [foldTag] and the web's are both locale-independent lowercasing, so the
* mirror holds.
*/
fun resolvedLabelColor(
name: String,
@@ -98,5 +98,12 @@ fun resolvedLabelColor(
when {
color.isNotEmpty() && color != "default" && color in known -> color
name.isEmpty() -> "default"
else -> derivedTint(name.lowercase())
else -> derivedTint(foldTag(name))
}
/**
* How two tag names compare: equal once every letter is lowercased, accented ones
* included, so `#Café` and `#CAFÉ` are one tag. The core's `derive::fold`, the server's
* `labeling.named` and the web's `foldTag` fold the same way (#5385).
*/
fun foldTag(name: String): String = name.lowercase()
@@ -366,11 +366,11 @@ private fun tintTags(
// A tag the note does not carry as a label yet — just typed, not yet
// derived — still gets a colour: `labelTint` falls back to deriving one
// from the name, which is what the chip would have shown anyway.
val picked = note.labels.associate { it.name.lowercase() to it.color }
val picked = note.labels.associate { foldTag(it.name) to it.color }
buildAnnotatedString {
append(text)
spans.forEach { tag ->
val tint = labelTint(tag.name, picked[tag.name.lowercase()].orEmpty())
val tint = labelTint(tag.name, picked[foldTag(tag.name)].orEmpty())
addStyle(
SpanStyle(color = tint.tagInk(dark), fontWeight = FontWeight.Medium),
tag.start.toInt(),
@@ -401,7 +401,7 @@ private fun RenameDialog(
var text by remember(tag.id) { mutableStateOf(tag.name) }
val trimmed = text.trim()
val clash =
others.firstOrNull { it.id != tag.id && it.name.equals(trimmed, ignoreCase = true) }
others.firstOrNull { it.id != tag.id && foldTag(it.name) == foldTag(trimmed) }
val submit = {
when {
trimmed.isEmpty() -> Unit