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

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());