fix(llc): normalize strings when comparing ComparableField - #2809
Conversation
Client-side sorts using `SortOption` on string fields no longer push lowercase names or non-ASCII names to the end of the list — the comparator now folds diacritics/ligatures and ignores case, mirroring the backend's `NormalizeName` pipeline (fold Latin diacritics, preserve Japanese/Thai/ Vietnamese-specific runes, lowercase, trim ASCII apostrophes and whitespace). Fixes #2601. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthrough
ChangesServer-parity string sorting
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2809 +/- ##
==========================================
+ Coverage 71.24% 71.27% +0.03%
==========================================
Files 429 430 +1
Lines 26893 26925 +32
==========================================
+ Hits 19159 19191 +32
Misses 7734 7734 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/stream_chat/lib/src/core/models/comparable_field.dart (1)
42-47: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider caching the normalized key instead of recomputing it on every comparison.
_compareStringsnormalizes both operands every timecompareToruns. During a full sort, this repeats normalization for the same value across O(n log n) comparisons instead of computing it once per element. For typical name-length strings and moderate list sizes this is unlikely to matter, but ifComparableFieldis ever used to sort large lists repeatedly, caching the normalized key atfromValueconstruction (only for theStringcase) would avoid the redundant work.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stream_chat/lib/src/core/models/comparable_field.dart` around lines 42 - 47, The _compareStrings method recomputes normalized sort keys for every comparison. Cache the normalized string when ComparableField is created through fromValue for String inputs, then have _compareStrings reuse those cached keys while preserving current comparison behavior for non-String values.packages/stream_chat/lib/src/core/util/string_sort_normalizer.dart (1)
49-56: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valuePer-rune calls to
removeDiacriticsre-allocate/lookup for every character.
_foldRuneinvokesremoveDiacritics(String.fromCharCode(rune))once per rune instead of batching the fold-eligible runes into a single call. Functionally equivalent (the underlying table is a per-code-unit map), but it's more allocation-heavy than necessary for longer strings.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stream_chat/lib/src/core/util/string_sort_normalizer.dart` around lines 49 - 56, Update the string normalization flow around _foldRune to batch fold-eligible runes into a single removeDiacritics call rather than invoking it once per rune. Preserve direct passthrough behavior for ASCII, Japanese, Thai, and Vietnamese-specific runes, while accumulating other runes and applying the existing diacritic removal in one operation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/stream_chat/lib/src/core/models/comparable_field.dart`:
- Around line 42-47: The _compareStrings method recomputes normalized sort keys
for every comparison. Cache the normalized string when ComparableField is
created through fromValue for String inputs, then have _compareStrings reuse
those cached keys while preserving current comparison behavior for non-String
values.
In `@packages/stream_chat/lib/src/core/util/string_sort_normalizer.dart`:
- Around line 49-56: Update the string normalization flow around _foldRune to
batch fold-eligible runes into a single removeDiacritics call rather than
invoking it once per rune. Preserve direct passthrough behavior for ASCII,
Japanese, Thai, and Vietnamese-specific runes, while accumulating other runes
and applying the existing diacritic removal in one operation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 529603a9-f582-46d4-99b0-4282af4be337
📒 Files selected for processing (7)
packages/stream_chat/CHANGELOG.mdpackages/stream_chat/lib/src/core/api/sort_order.dartpackages/stream_chat/lib/src/core/models/comparable_field.dartpackages/stream_chat/lib/src/core/util/string_sort_normalizer.dartpackages/stream_chat/pubspec.yamlpackages/stream_chat/test/src/core/models/comparable_field_test.dartpackages/stream_chat/test/src/core/util/string_sort_normalizer_test.dart
|
The Root cause — firebase/flutterfire#18438 (opened 2026-07-13, currently open):
Same failure exists on plain Plan — wait for Firebase to either publish the missing |
Summary
Fixes #2601 — client-side
SortOptiononname(and any other string field) was returning results in a different order than the server becauseComparableFieldused a raw codepoint compare. Lowercase names and non-ASCII names (PolishŁ, NorwegianØ, etc.) got pushed to the end instead of sorting alphabetically.Root cause:
ComparableField.compareTo'sStringbranch wasa.compareTo(b), i.e. byte-order on UTF-16 code units.Fix: route string comparisons through a new
normalizeStringForSorthelper that mirrors the backend'sNormalizeNamepipeline:é → e,Ł → l,Æ → ae) viapackage:diacritic.Normalizeearly-exit — critical for Japanese combining sound marks in the Hiragana range).Backend has no tie-breaker for
sort: name, so we don't either — equal-normalized values compare as0and Dart's stableList.sortpreserves input order.Notes
SortOption'scomparatorparameter remains the escape hatch for callers who need raw codepoint or locale-aware behavior.Mncategories.diacritic: ^0.1.6tostream_chat(already used bystream_chat_flutter).Test plan
packages/stream_chat/test/src/core/util/string_sort_normalizer_test.dart— ports 10 applicable cases from backend'sTestNormalizeName+ coverage for Vietnamese preservation, ligature folding, combining marks, apostrophe/whitespace trimming, and Japanese combining sound marks (U+3099 / U+309A).comparable_field_test.dartthat sorts the exact scenario from the bug report (Zara,jhon,Łukasz,Øystein,Adam,Marek) and verifies the expected alphabetical order.stream_chatsuite (1551 tests) passes.dart analyze --fatal-infos .clean.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Documentation