Do proper Unicode collation on room names (#440)

This commit is contained in:
Ulyssa 2025-05-31 12:52:15 -07:00 committed by GitHub
parent 9ed9400b67
commit ba7d0392d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 85 additions and 21 deletions

40
Cargo.lock generated
View file

@ -579,6 +579,15 @@ version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bit_field"
version = "0.10.2"
@ -674,6 +683,17 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "bstr"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "built"
version = "0.7.7"
@ -1619,6 +1639,19 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "feruca"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06eccaab9dc53ad4bffb4ed748baf5c1f9475d5e9cac35e1b8eac69dac56899e"
dependencies = [
"bincode",
"bstr",
"once_cell",
"rustc-hash",
"unicode-canonical-combining-class",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
@ -2198,6 +2231,7 @@ dependencies = [
"dirs",
"edit",
"emojis",
"feruca",
"futures",
"gethostname",
"html5ever",
@ -5713,6 +5747,12 @@ version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-canonical-combining-class"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41c99d5174052d02ce765418e826597a1be18f32c114e35d9e22f92390239561"
[[package]]
name = "unicode-ident"
version = "1.0.18"

View file

@ -34,6 +34,7 @@ clap = {version = "~4.3", features = ["derive"]}
css-color-parser = "0.1.2"
dirs = "4.0.0"
emojis = "0.5"
feruca = "0.10.1"
futures = "0.3"
gethostname = "0.4.1"
html5ever = "0.26.0"

View file

@ -1486,6 +1486,9 @@ pub struct ChatStore {
/// Whether the application is currently focused
pub focused: bool,
/// Collator for locale-aware text sorting.
pub collator: feruca::Collator,
}
impl ChatStore {
@ -1500,6 +1503,7 @@ impl ChatStore {
cmds: crate::commands::setup_commands(),
emojis: emoji_map(),
collator: Default::default(),
names: Default::default(),
rooms: Default::default(),
presences: Default::default(),

View file

@ -1,7 +1,7 @@
//! # Logic for loading and validating application configuration
use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::fs::File;
use std::hash::{Hash, Hasher};
@ -105,7 +105,7 @@ fn validate_profile_name(name: &str) -> bool {
name.chars().all(is_profile_char)
}
fn validate_profile_names(names: &HashMap<String, ProfileConfig>) {
fn validate_profile_names(names: &BTreeMap<String, ProfileConfig>) {
for name in names.keys() {
if validate_profile_name(name.as_str()) {
continue;
@ -787,7 +787,7 @@ pub struct ProfileConfig {
#[derive(Clone, Deserialize)]
pub struct IambConfig {
pub profiles: HashMap<String, ProfileConfig>,
pub profiles: BTreeMap<String, ProfileConfig>,
pub default_profile: Option<String>,
pub settings: Option<Tunables>,
pub dirs: Option<Directories>,

View file

@ -82,6 +82,7 @@ use crate::base::{
use self::{room::RoomState, welcome::WelcomeState};
use crate::message::MessageTimeStamp;
use feruca::Collator;
pub mod room;
pub mod welcome;
@ -170,7 +171,12 @@ fn user_cmp(a: &MemberItem, b: &MemberItem, field: &SortFieldUser) -> Ordering {
}
}
fn room_cmp<T: RoomLikeItem>(a: &T, b: &T, field: &SortFieldRoom) -> Ordering {
fn room_cmp<T: RoomLikeItem>(
a: &T,
b: &T,
field: &SortFieldRoom,
collator: &mut Collator,
) -> Ordering {
match field {
SortFieldRoom::Favorite => {
let fava = a.has_tag(TagName::Favorite);
@ -186,7 +192,7 @@ fn room_cmp<T: RoomLikeItem>(a: &T, b: &T, field: &SortFieldRoom) -> Ordering {
// If a has LowPriority and b doesn't, it should sort later in room list.
lowa.cmp(&lowb)
},
SortFieldRoom::Name => a.name().cmp(b.name()),
SortFieldRoom::Name => collator.collate(a.name(), b.name()),
SortFieldRoom::Alias => some_cmp(a.alias(), b.alias(), Ord::cmp),
SortFieldRoom::RoomId => a.room_id().cmp(b.room_id()),
SortFieldRoom::Unread => {
@ -209,9 +215,10 @@ fn room_fields_cmp<T: RoomLikeItem>(
a: &T,
b: &T,
fields: &[SortColumn<SortFieldRoom>],
collator: &mut Collator,
) -> Ordering {
for SortColumn(field, order) in fields {
match (room_cmp(a, b, field), order) {
match (room_cmp(a, b, field, collator), order) {
(Ordering::Equal, _) => continue,
(o, SortOrder::Ascending) => return o,
(o, SortOrder::Descending) => return o.reverse(),
@ -219,7 +226,7 @@ fn room_fields_cmp<T: RoomLikeItem>(
}
// Break ties on ascending room id.
room_cmp(a, b, &SortFieldRoom::RoomId)
room_cmp(a, b, &SortFieldRoom::RoomId, collator)
}
fn user_fields_cmp(
@ -516,7 +523,8 @@ impl WindowOps<IambInfo> for IambWindow {
.map(|room_info| DirectItem::new(room_info, store))
.collect::<Vec<_>>();
let fields = &store.application.settings.tunables.sort.dms;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
let collator = &mut store.application.collator;
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
state.set(items);
@ -561,7 +569,8 @@ impl WindowOps<IambInfo> for IambWindow {
.map(|room_info| RoomItem::new(room_info, store))
.collect::<Vec<_>>();
let fields = &store.application.settings.tunables.sort.rooms;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
let collator = &mut store.application.collator;
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
state.set(items);
@ -592,7 +601,8 @@ impl WindowOps<IambInfo> for IambWindow {
items.extend(dms);
let fields = &store.application.settings.tunables.sort.chats;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
let collator = &mut store.application.collator;
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
state.set(items);
@ -625,7 +635,8 @@ impl WindowOps<IambInfo> for IambWindow {
items.extend(dms);
let fields = &store.application.settings.tunables.sort.chats;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
let collator = &mut store.application.collator;
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
state.set(items);
@ -645,7 +656,8 @@ impl WindowOps<IambInfo> for IambWindow {
.map(|room| SpaceItem::new(room, store))
.collect::<Vec<_>>();
let fields = &store.application.settings.tunables.sort.spaces;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
let collator = &mut store.application.collator;
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
state.set(items);
@ -1627,6 +1639,8 @@ mod tests {
#[test]
fn test_sort_rooms() {
let mut collator = Collator::default();
let collator = &mut collator;
let server = server_name!("example.com");
let room1 = TestRoomItem {
@ -1659,13 +1673,13 @@ mod tests {
// Sort by Name ascending.
let mut rooms = vec![&room1, &room2, &room3];
let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Ascending)];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
assert_eq!(rooms, vec![&room3, &room2, &room1]);
// Sort by Name descending.
let mut rooms = vec![&room1, &room2, &room3];
let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Descending)];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
assert_eq!(rooms, vec![&room1, &room2, &room3]);
// Sort by Favorite and Alias before Name to show order matters.
@ -1675,7 +1689,7 @@ mod tests {
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
assert_eq!(rooms, vec![&room1, &room2, &room3]);
// Now flip order of Favorite with Descending
@ -1685,12 +1699,14 @@ mod tests {
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
assert_eq!(rooms, vec![&room2, &room3, &room1]);
}
#[test]
fn test_sort_room_recents() {
let mut collator = Collator::default();
let collator = &mut collator;
let server = server_name!("example.com");
let room1 = TestRoomItem {
@ -1729,18 +1745,20 @@ mod tests {
// Sort by Recent ascending.
let mut rooms = vec![&room1, &room2, &room3];
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Ascending)];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
assert_eq!(rooms, vec![&room2, &room3, &room1]);
// Sort by Recent descending.
let mut rooms = vec![&room1, &room2, &room3];
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Descending)];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
assert_eq!(rooms, vec![&room1, &room3, &room2]);
}
#[test]
fn test_sort_room_invites() {
let mut collator = Collator::default();
let collator = &mut collator;
let server = server_name!("example.com");
let room1 = TestRoomItem {
@ -1776,7 +1794,7 @@ mod tests {
SortColumn(SortFieldRoom::Invite, SortOrder::Ascending),
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
assert_eq!(rooms, vec![&room3, &room1, &room2]);
// Sort invites after
@ -1785,7 +1803,7 @@ mod tests {
SortColumn(SortFieldRoom::Invite, SortOrder::Descending),
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
assert_eq!(rooms, vec![&room1, &room2, &room3]);
}
}

View file

@ -214,7 +214,8 @@ impl StatefulWidget for Space<'_> {
})
.collect::<Vec<_>>();
let fields = &self.store.application.settings.tunables.sort.rooms;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
let collator = &mut self.store.application.collator;
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
state.list.set(items);
state.last_fetch = Some(Instant::now());