Support custom sorting for room and user lists (#170)

This commit is contained in:
Ulyssa 2023-10-20 19:32:33 -07:00 committed by GitHub
parent 443ad241b4
commit 8943909f06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 500 additions and 119 deletions

View file

@ -1,6 +1,6 @@
# iamb # iamb
[![Build Status](https://github.com/ulyssa/iamb/workflows/CI/badge.svg)](https://github.com/ulyssa/iamb/actions?query=workflow%3ACI+) [![Build Status](https://github.com/ulyssa/iamb/actions/workflows/ci.yml/badge.svg)](https://github.com/ulyssa/iamb/actions?query=workflow%3ACI+)
[![License: Apache 2.0](https://img.shields.io/crates/l/iamb.svg?logo=apache)](https://crates.io/crates/iamb) [![License: Apache 2.0](https://img.shields.io/crates/l/iamb.svg?logo=apache)](https://crates.io/crates/iamb)
[![#iamb:0x.badd.cafe](https://img.shields.io/badge/matrix-%23iamb:0x.badd.cafe-blue)](https://matrix.to/#/#iamb:0x.badd.cafe) [![#iamb:0x.badd.cafe](https://img.shields.io/badge/matrix-%23iamb:0x.badd.cafe-blue)](https://matrix.to/#/#iamb:0x.badd.cafe)
[![Latest Version](https://img.shields.io/crates/v/iamb.svg?logo=rust)](https://crates.io/crates/iamb) [![Latest Version](https://img.shields.io/crates/v/iamb.svg?logo=rust)](https://crates.io/crates/iamb)

View file

@ -202,6 +202,135 @@ bitflags::bitflags! {
} }
} }
/// Fields that rooms and spaces can be sorted by.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SortFieldRoom {
Favorite,
LowPriority,
Name,
Alias,
RoomId,
}
/// Fields that users can be sorted by.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SortFieldUser {
PowerLevel,
UserId,
LocalPart,
Server,
}
/// Whether to use the default sort direction for a field, or to reverse it.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SortOrder {
Ascending,
Descending,
}
/// One of the columns to sort on.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SortColumn<T>(pub T, pub SortOrder);
impl<'de> Deserialize<'de> for SortColumn<SortFieldRoom> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(SortRoomVisitor)
}
}
/// [serde] visitor for deserializing [SortColumn] for rooms and spaces.
struct SortRoomVisitor;
impl<'de> Visitor<'de> for SortRoomVisitor {
type Value = SortColumn<SortFieldRoom>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid field for sorting rooms")
}
fn visit_str<E>(self, mut value: &str) -> Result<Self::Value, E>
where
E: SerdeError,
{
if value.is_empty() {
return Err(E::custom("Invalid sort field"));
}
let order = if value.starts_with('~') {
value = &value[1..];
SortOrder::Descending
} else {
SortOrder::Ascending
};
let field = match value {
"favorite" => SortFieldRoom::Favorite,
"lowpriority" => SortFieldRoom::LowPriority,
"name" => SortFieldRoom::Name,
"alias" => SortFieldRoom::Alias,
"id" => SortFieldRoom::RoomId,
_ => {
let msg = format!("Unknown sort field: {value:?}");
return Err(E::custom(msg));
},
};
Ok(SortColumn(field, order))
}
}
impl<'de> Deserialize<'de> for SortColumn<SortFieldUser> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(SortUserVisitor)
}
}
/// [serde] visitor for deserializing [SortColumn] for users.
struct SortUserVisitor;
impl<'de> Visitor<'de> for SortUserVisitor {
type Value = SortColumn<SortFieldUser>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid field for sorting rooms")
}
fn visit_str<E>(self, mut value: &str) -> Result<Self::Value, E>
where
E: SerdeError,
{
if value.is_empty() {
return Err(E::custom("Invalid field for sorting users"));
}
let order = if value.starts_with('~') {
value = &value[1..];
SortOrder::Descending
} else {
SortOrder::Ascending
};
let field = match value {
"id" => SortFieldUser::UserId,
"localpart" => SortFieldUser::LocalPart,
"server" => SortFieldUser::Server,
"power" => SortFieldUser::PowerLevel,
_ => {
let msg = format!("Unknown sort field: {value:?}");
return Err(E::custom(msg));
},
};
Ok(SortColumn(field, order))
}
}
/// A room property. /// A room property.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum RoomField { pub enum RoomField {
@ -811,7 +940,7 @@ fn emoji_map() -> CompletionMap<String, &'static Emoji> {
#[derive(Default)] #[derive(Default)]
pub struct SyncInfo { pub struct SyncInfo {
/// Spaces that the user is a member of. /// Spaces that the user is a member of.
pub spaces: Vec<MatrixRoom>, pub spaces: Vec<Arc<(MatrixRoom, Option<Tags>)>>,
/// Rooms that the user is a member of. /// Rooms that the user is a member of.
pub rooms: Vec<Arc<(MatrixRoom, Option<Tags>)>>, pub rooms: Vec<Arc<(MatrixRoom, Option<Tags>)>>,

View file

@ -20,7 +20,7 @@ use modalkit::tui::{
text::Span, text::Span,
}; };
use super::base::{IambId, RoomInfo}; use super::base::{IambId, RoomInfo, SortColumn, SortFieldRoom, SortFieldUser, SortOrder};
macro_rules! usage { macro_rules! usage {
( $($args: tt)* ) => { ( $($args: tt)* ) => {
@ -29,6 +29,17 @@ macro_rules! usage {
} }
} }
const DEFAULT_MEMBERS_SORT: [SortColumn<SortFieldUser>; 2] = [
SortColumn(SortFieldUser::PowerLevel, SortOrder::Ascending),
SortColumn(SortFieldUser::UserId, SortOrder::Ascending),
];
const DEFAULT_ROOM_SORT: [SortColumn<SortFieldRoom>; 3] = [
SortColumn(SortFieldRoom::Favorite, SortOrder::Ascending),
SortColumn(SortFieldRoom::LowPriority, SortOrder::Ascending),
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
];
const DEFAULT_REQ_TIMEOUT: u64 = 120; const DEFAULT_REQ_TIMEOUT: u64 = 120;
const COLORS: [Color; 13] = [ const COLORS: [Color; 13] = [
@ -213,6 +224,15 @@ pub struct UserDisplayTunables {
pub type UserOverrides = HashMap<OwnedUserId, UserDisplayTunables>; pub type UserOverrides = HashMap<OwnedUserId, UserDisplayTunables>;
fn merge_sorts(a: SortOverrides, b: SortOverrides) -> SortOverrides {
SortOverrides {
dms: b.dms.or(a.dms),
rooms: b.rooms.or(a.rooms),
spaces: b.spaces.or(a.spaces),
members: b.members.or(a.members),
}
}
fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<UserOverrides> { fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<UserOverrides> {
match (a, b) { match (a, b) {
(Some(a), None) => Some(a), (Some(a), None) => Some(a),
@ -246,6 +266,33 @@ pub enum UserDisplayStyle {
DisplayName, DisplayName,
} }
#[derive(Clone)]
pub struct SortValues {
pub dms: Vec<SortColumn<SortFieldRoom>>,
pub rooms: Vec<SortColumn<SortFieldRoom>>,
pub spaces: Vec<SortColumn<SortFieldRoom>>,
pub members: Vec<SortColumn<SortFieldUser>>,
}
#[derive(Clone, Default, Deserialize)]
pub struct SortOverrides {
pub dms: Option<Vec<SortColumn<SortFieldRoom>>>,
pub rooms: Option<Vec<SortColumn<SortFieldRoom>>>,
pub spaces: Option<Vec<SortColumn<SortFieldRoom>>>,
pub members: Option<Vec<SortColumn<SortFieldUser>>>,
}
impl SortOverrides {
pub fn values(self) -> SortValues {
let rooms = self.rooms.unwrap_or_else(|| Vec::from(DEFAULT_ROOM_SORT));
let dms = self.dms.unwrap_or_else(|| rooms.clone());
let spaces = self.spaces.unwrap_or_else(|| rooms.clone());
let members = self.members.unwrap_or_else(|| Vec::from(DEFAULT_MEMBERS_SORT));
SortValues { rooms, members, dms, spaces }
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct TunableValues { pub struct TunableValues {
pub log_level: Level, pub log_level: Level,
@ -254,6 +301,7 @@ pub struct TunableValues {
pub read_receipt_send: bool, pub read_receipt_send: bool,
pub read_receipt_display: bool, pub read_receipt_display: bool,
pub request_timeout: u64, pub request_timeout: u64,
pub sort: SortValues,
pub typing_notice_send: bool, pub typing_notice_send: bool,
pub typing_notice_display: bool, pub typing_notice_display: bool,
pub users: UserOverrides, pub users: UserOverrides,
@ -270,6 +318,8 @@ pub struct Tunables {
pub read_receipt_send: Option<bool>, pub read_receipt_send: Option<bool>,
pub read_receipt_display: Option<bool>, pub read_receipt_display: Option<bool>,
pub request_timeout: Option<u64>, pub request_timeout: Option<u64>,
#[serde(default)]
pub sort: SortOverrides,
pub typing_notice_send: Option<bool>, pub typing_notice_send: Option<bool>,
pub typing_notice_display: Option<bool>, pub typing_notice_display: Option<bool>,
pub users: Option<UserOverrides>, pub users: Option<UserOverrides>,
@ -289,6 +339,7 @@ impl Tunables {
read_receipt_send: self.read_receipt_send.or(other.read_receipt_send), read_receipt_send: self.read_receipt_send.or(other.read_receipt_send),
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display), read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
request_timeout: self.request_timeout.or(other.request_timeout), request_timeout: self.request_timeout.or(other.request_timeout),
sort: merge_sorts(self.sort, other.sort),
typing_notice_send: self.typing_notice_send.or(other.typing_notice_send), typing_notice_send: self.typing_notice_send.or(other.typing_notice_send),
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display), typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
users: merge_users(self.users, other.users), users: merge_users(self.users, other.users),
@ -306,6 +357,7 @@ impl Tunables {
read_receipt_send: self.read_receipt_send.unwrap_or(true), read_receipt_send: self.read_receipt_send.unwrap_or(true),
read_receipt_display: self.read_receipt_display.unwrap_or(true), read_receipt_display: self.read_receipt_display.unwrap_or(true),
request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQ_TIMEOUT), request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQ_TIMEOUT),
sort: self.sort.values(),
typing_notice_send: self.typing_notice_send.unwrap_or(true), typing_notice_send: self.typing_notice_send.unwrap_or(true),
typing_notice_display: self.typing_notice_display.unwrap_or(true), typing_notice_display: self.typing_notice_display.unwrap_or(true),
users: self.users.unwrap_or_default(), users: self.users.unwrap_or_default(),
@ -701,6 +753,43 @@ mod tests {
assert_eq!(res.username_display, Some(UserDisplayStyle::DisplayName)); assert_eq!(res.username_display, Some(UserDisplayStyle::DisplayName));
} }
#[test]
fn test_parse_tunables_sort() {
let res: Tunables = serde_json::from_str(
r#"{"sort": {"members": ["server","~localpart"],"spaces":["~favorite", "alias"]}}"#,
)
.unwrap();
assert_eq!(
res.sort.members,
Some(vec![
SortColumn(SortFieldUser::Server, SortOrder::Ascending),
SortColumn(SortFieldUser::LocalPart, SortOrder::Descending),
])
);
assert_eq!(
res.sort.spaces,
Some(vec![
SortColumn(SortFieldRoom::Favorite, SortOrder::Descending),
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
])
);
assert_eq!(res.sort.rooms, None);
assert_eq!(res.sort.dms, None);
// Check that we get the right default "rooms" and "dms" values.
let res = res.values();
assert_eq!(res.sort.members, vec![
SortColumn(SortFieldUser::Server, SortOrder::Ascending),
SortColumn(SortFieldUser::LocalPart, SortOrder::Descending),
]);
assert_eq!(res.sort.spaces, vec![
SortColumn(SortFieldRoom::Favorite, SortOrder::Descending),
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
]);
assert_eq!(res.sort.rooms, Vec::from(DEFAULT_ROOM_SORT));
assert_eq!(res.sort.dms, Vec::from(DEFAULT_ROOM_SORT));
}
#[test] #[test]
fn test_parse_layout() { fn test_parse_layout() {
let user = WindowPath::UserId(user_id!("@user:example.com").to_owned()); let user = WindowPath::UserId(user_id!("@user:example.com").to_owned());

View file

@ -28,6 +28,7 @@ use crate::{
ApplicationSettings, ApplicationSettings,
DirectoryValues, DirectoryValues,
ProfileConfig, ProfileConfig,
SortOverrides,
TunableValues, TunableValues,
UserColor, UserColor,
UserDisplayStyle, UserDisplayStyle,
@ -183,6 +184,7 @@ pub fn mock_tunables() -> TunableValues {
read_receipt_send: true, read_receipt_send: true,
read_receipt_display: true, read_receipt_display: true,
request_timeout: 120, request_timeout: 120,
sort: SortOverrides::default().values(),
typing_notice_send: true, typing_notice_send: true,
typing_notice_display: true, typing_notice_display: true,
users: vec![(TEST_USER5.clone(), UserDisplayTunables { users: vec![(TEST_USER5.clone(), UserDisplayTunables {

View file

@ -17,7 +17,9 @@ use matrix_sdk::{
ruma::{ ruma::{
events::room::member::MembershipState, events::room::member::MembershipState,
events::tag::{TagName, Tags}, events::tag::{TagName, Tags},
OwnedRoomAliasId,
OwnedRoomId, OwnedRoomId,
RoomAliasId,
RoomId, RoomId,
}, },
}; };
@ -80,6 +82,10 @@ use crate::base::{
ProgramStore, ProgramStore,
RoomAction, RoomAction,
SendAction, SendAction,
SortColumn,
SortFieldRoom,
SortFieldUser,
SortOrder,
}; };
use self::{room::RoomState, welcome::WelcomeState}; use self::{room::RoomState, welcome::WelcomeState};
@ -125,42 +131,87 @@ fn selected_text(s: &str, selected: bool) -> Text {
Text::from(selected_span(s, selected)) Text::from(selected_span(s, selected))
} }
fn room_cmp(a: &MatrixRoom, b: &MatrixRoom) -> Ordering { /// Sort `Some` to be less than `None` so that list items with values come before those without.
let ca1 = a.canonical_alias(); #[inline]
let ca2 = b.canonical_alias(); fn some_cmp<T: Ord>(a: Option<T>, b: Option<T>) -> Ordering {
match (a, b) {
let ord = match (ca1, ca2) { (Some(a), Some(b)) => a.cmp(&b),
(None, None) => Ordering::Equal, (None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater, (None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less, (Some(_), None) => Ordering::Less,
(Some(ca1), Some(ca2)) => ca1.cmp(&ca2), }
};
ord.then_with(|| a.room_id().cmp(b.room_id()))
} }
fn tag_cmp(a: &Option<Tags>, b: &Option<Tags>) -> Ordering { fn user_cmp(a: &MemberItem, b: &MemberItem, field: &SortFieldUser) -> Ordering {
let (fava, lowa) = a let a_id = a.member.user_id();
.as_ref() let b_id = b.member.user_id();
.map(|tags| {
(tags.contains_key(&TagName::Favorite), tags.contains_key(&TagName::LowPriority))
})
.unwrap_or((false, false));
let (favb, lowb) = b match field {
.as_ref() SortFieldUser::UserId => a_id.cmp(b_id),
.map(|tags| { SortFieldUser::LocalPart => a_id.localpart().cmp(b_id.localpart()),
(tags.contains_key(&TagName::Favorite), tags.contains_key(&TagName::LowPriority)) SortFieldUser::Server => a_id.server_name().cmp(b_id.server_name()),
}) SortFieldUser::PowerLevel => {
.unwrap_or((false, false)); // Sort higher power levels towards the top of the list.
b.member.power_level().cmp(&a.member.power_level())
},
}
}
fn room_cmp<T: RoomLikeItem>(a: &T, b: &T, field: &SortFieldRoom) -> Ordering {
match field {
SortFieldRoom::Favorite => {
let fava = a.has_tag(TagName::Favorite);
let favb = b.has_tag(TagName::Favorite);
// If a has Favorite and b doesn't, it should sort earlier in room list. // If a has Favorite and b doesn't, it should sort earlier in room list.
let cmpf = favb.cmp(&fava); favb.cmp(&fava)
},
SortFieldRoom::LowPriority => {
let lowa = a.has_tag(TagName::LowPriority);
let lowb = b.has_tag(TagName::LowPriority);
// If a has LowPriority and b doesn't, it should sort later in room list. // If a has LowPriority and b doesn't, it should sort later in room list.
let cmpl = lowa.cmp(&lowb); lowa.cmp(&lowb)
},
SortFieldRoom::Name => a.name().cmp(b.name()),
SortFieldRoom::Alias => some_cmp(a.alias(), b.alias()),
SortFieldRoom::RoomId => a.room_id().cmp(b.room_id()),
}
}
cmpl.then(cmpf) /// Compare two rooms according the configured sort criteria.
fn room_fields_cmp<T: RoomLikeItem>(
a: &T,
b: &T,
fields: &[SortColumn<SortFieldRoom>],
) -> Ordering {
for SortColumn(field, order) in fields {
match (room_cmp(a, b, field), order) {
(Ordering::Equal, _) => continue,
(o, SortOrder::Ascending) => return o,
(o, SortOrder::Descending) => return o.reverse(),
}
}
// Break ties on ascending room id.
room_cmp(a, b, &SortFieldRoom::RoomId)
}
fn user_fields_cmp(
a: &MemberItem,
b: &MemberItem,
fields: &[SortColumn<SortFieldUser>],
) -> Ordering {
for SortColumn(field, order) in fields {
match (user_cmp(a, b, field), order) {
(Ordering::Equal, _) => continue,
(o, SortOrder::Ascending) => return o,
(o, SortOrder::Descending) => return o.reverse(),
}
}
// Break ties on ascending user id.
user_cmp(a, b, &SortFieldUser::UserId)
} }
fn append_tags<'a>(tags: &'a Tags, spans: &mut Vec<Span<'a>>, style: Style) { fn append_tags<'a>(tags: &'a Tags, spans: &mut Vec<Span<'a>>, style: Style) {
@ -190,6 +241,13 @@ fn append_tags<'a>(tags: &'a Tags, spans: &mut Vec<Span<'a>>, style: Style) {
spans.push(Span::styled(")", style)); spans.push(Span::styled(")", style));
} }
trait RoomLikeItem {
fn room_id(&self) -> &RoomId;
fn has_tag(&self, tag: TagName) -> bool;
fn alias(&self) -> Option<&RoomAliasId>;
fn name(&self) -> &str;
}
#[inline] #[inline]
fn room_prompt( fn room_prompt(
room_id: &RoomId, room_id: &RoomId,
@ -399,7 +457,8 @@ impl WindowOps<IambInfo> for IambWindow {
.into_iter() .into_iter()
.map(|room_info| DirectItem::new(room_info, store)) .map(|room_info| DirectItem::new(room_info, store))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
items.sort(); let fields = &store.application.settings.tunables.sort.dms;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
state.set(items); state.set(items);
@ -417,8 +476,13 @@ impl WindowOps<IambInfo> for IambWindow {
if need_fetch { if need_fetch {
if let Ok(mems) = store.application.worker.members(room_id.clone()) { if let Ok(mems) = store.application.worker.members(room_id.clone()) {
let items = mems.into_iter().map(|m| MemberItem::new(m, room_id.clone())); let mut items = mems
state.set(items.collect()); .into_iter()
.map(|m| MemberItem::new(m, room_id.clone()))
.collect::<Vec<_>>();
let fields = &store.application.settings.tunables.sort.members;
items.sort_by(|a, b| user_fields_cmp(a, b, fields));
state.set(items);
*last_fetch = Some(Instant::now()); *last_fetch = Some(Instant::now());
} }
} }
@ -438,7 +502,8 @@ impl WindowOps<IambInfo> for IambWindow {
.into_iter() .into_iter()
.map(|room_info| RoomItem::new(room_info, store)) .map(|room_info| RoomItem::new(room_info, store))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
items.sort(); let fields = &store.application.settings.tunables.sort.rooms;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
state.set(items); state.set(items);
@ -449,15 +514,18 @@ impl WindowOps<IambInfo> for IambWindow {
.render(area, buf, state); .render(area, buf, state);
}, },
IambWindow::SpaceList(state) => { IambWindow::SpaceList(state) => {
let items = store let mut items = store
.application .application
.sync_info .sync_info
.spaces .spaces
.clone() .clone()
.into_iter() .into_iter()
.map(|room| SpaceItem::new(room, store)); .map(|room| SpaceItem::new(room, store))
state.set(items.collect()); .collect::<Vec<_>>();
state.draw(area, buf, focused, store); let fields = &store.application.settings.tunables.sort.spaces;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
state.set(items);
List::new(store) List::new(store)
.empty_message("You haven't joined any spaces yet") .empty_message("You haven't joined any spaces yet")
@ -662,6 +730,7 @@ impl Window<IambInfo> for IambWindow {
pub struct RoomItem { pub struct RoomItem {
room_info: MatrixRoomInfo, room_info: MatrixRoomInfo,
name: String, name: String,
alias: Option<OwnedRoomAliasId>,
} }
impl RoomItem { impl RoomItem {
@ -671,13 +740,14 @@ impl RoomItem {
let info = store.application.get_room_info(room_id.to_owned()); let info = store.application.get_room_info(room_id.to_owned());
let name = info.name.clone().unwrap_or_default(); let name = info.name.clone().unwrap_or_default();
let alias = room.canonical_alias();
info.tags = room_info.deref().1.clone(); info.tags = room_info.deref().1.clone();
if let Some(alias) = room.canonical_alias() { if let Some(alias) = &alias {
store.application.names.insert(alias.to_string(), room_id.to_owned()); store.application.names.insert(alias.to_string(), room_id.to_owned());
} }
RoomItem { room_info, name } RoomItem { room_info, name, alias }
} }
#[inline] #[inline]
@ -685,34 +755,31 @@ impl RoomItem {
&self.room_info.deref().0 &self.room_info.deref().0
} }
#[inline]
fn room_id(&self) -> &RoomId {
self.room().room_id()
}
#[inline] #[inline]
fn tags(&self) -> &Option<Tags> { fn tags(&self) -> &Option<Tags> {
&self.room_info.deref().1 &self.room_info.deref().1
} }
} }
impl PartialEq for RoomItem { impl RoomLikeItem for RoomItem {
fn eq(&self, other: &Self) -> bool { fn name(&self) -> &str {
self.room_id() == other.room_id() self.name.as_str()
}
} }
impl Eq for RoomItem {} fn alias(&self) -> Option<&RoomAliasId> {
self.alias.as_deref()
impl Ord for RoomItem {
fn cmp(&self, other: &Self) -> Ordering {
tag_cmp(self.tags(), other.tags()).then_with(|| room_cmp(self.room(), other.room()))
}
} }
impl PartialOrd for RoomItem { fn room_id(&self) -> &RoomId {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { self.room().room_id()
self.cmp(other).into() }
fn has_tag(&self, tag: TagName) -> bool {
if let Some(tags) = &self.room_info.deref().1 {
tags.contains_key(&tag)
} else {
false
}
} }
} }
@ -756,14 +823,16 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for RoomItem {
pub struct DirectItem { pub struct DirectItem {
room_info: MatrixRoomInfo, room_info: MatrixRoomInfo,
name: String, name: String,
alias: Option<OwnedRoomAliasId>,
} }
impl DirectItem { impl DirectItem {
fn new(room_info: MatrixRoomInfo, store: &mut ProgramStore) -> Self { fn new(room_info: MatrixRoomInfo, store: &mut ProgramStore) -> Self {
let room_id = room_info.deref().0.room_id().to_owned(); let room_id = room_info.0.room_id().to_owned();
let name = store.application.get_room_info(room_id).name.clone().unwrap_or_default(); let name = store.application.get_room_info(room_id).name.clone().unwrap_or_default();
let alias = room_info.0.canonical_alias();
DirectItem { room_info, name } DirectItem { room_info, name, alias }
} }
#[inline] #[inline]
@ -771,17 +840,34 @@ impl DirectItem {
&self.room_info.deref().0 &self.room_info.deref().0
} }
#[inline]
fn room_id(&self) -> &RoomId {
self.room().room_id()
}
#[inline] #[inline]
fn tags(&self) -> &Option<Tags> { fn tags(&self) -> &Option<Tags> {
&self.room_info.deref().1 &self.room_info.deref().1
} }
} }
impl RoomLikeItem for DirectItem {
fn name(&self) -> &str {
self.name.as_str()
}
fn alias(&self) -> Option<&RoomAliasId> {
self.alias.as_deref()
}
fn has_tag(&self, tag: TagName) -> bool {
if let Some(tags) = &self.room_info.deref().1 {
tags.contains_key(&tag)
} else {
false
}
}
fn room_id(&self) -> &RoomId {
self.room().room_id()
}
}
impl ToString for DirectItem { impl ToString for DirectItem {
fn to_string(&self) -> String { fn to_string(&self) -> String {
return self.name.clone(); return self.name.clone();
@ -807,26 +893,6 @@ impl ListItem<IambInfo> for DirectItem {
} }
} }
impl PartialEq for DirectItem {
fn eq(&self, other: &Self) -> bool {
self.room_id() == other.room_id()
}
}
impl Eq for DirectItem {}
impl Ord for DirectItem {
fn cmp(&self, other: &Self) -> Ordering {
tag_cmp(self.tags(), other.tags()).then_with(|| room_cmp(self.room(), other.room()))
}
}
impl PartialOrd for DirectItem {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.cmp(other).into()
}
}
impl Promptable<ProgramContext, ProgramStore, IambInfo> for DirectItem { impl Promptable<ProgramContext, ProgramStore, IambInfo> for DirectItem {
fn prompt( fn prompt(
&mut self, &mut self,
@ -840,51 +906,58 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for DirectItem {
#[derive(Clone)] #[derive(Clone)]
pub struct SpaceItem { pub struct SpaceItem {
room: MatrixRoom, room_info: MatrixRoomInfo,
name: String, name: String,
alias: Option<OwnedRoomAliasId>,
} }
impl SpaceItem { impl SpaceItem {
fn new(room: MatrixRoom, store: &mut ProgramStore) -> Self { fn new(room_info: MatrixRoomInfo, store: &mut ProgramStore) -> Self {
let room_id = room.room_id(); let room_id = room_info.0.room_id();
let name = store let name = store
.application .application
.get_room_info(room_id.to_owned()) .get_room_info(room_id.to_owned())
.name .name
.clone() .clone()
.unwrap_or_default(); .unwrap_or_default();
let alias = room_info.0.canonical_alias();
if let Some(alias) = room.canonical_alias() { if let Some(alias) = &alias {
store.application.names.insert(alias.to_string(), room_id.to_owned()); store.application.names.insert(alias.to_string(), room_id.to_owned());
} }
SpaceItem { room, name } SpaceItem { room_info, name, alias }
}
#[inline]
fn room(&self) -> &MatrixRoom {
&self.room_info.deref().0
} }
} }
impl PartialEq for SpaceItem { impl RoomLikeItem for SpaceItem {
fn eq(&self, other: &Self) -> bool { fn name(&self) -> &str {
self.room.room_id() == other.room.room_id() self.name.as_str()
}
} }
impl Eq for SpaceItem {} fn room_id(&self) -> &RoomId {
self.room().room_id()
impl Ord for SpaceItem {
fn cmp(&self, other: &Self) -> Ordering {
room_cmp(&self.room, &other.room)
}
} }
impl PartialOrd for SpaceItem { fn alias(&self) -> Option<&RoomAliasId> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { self.alias.as_deref()
self.cmp(other).into() }
fn has_tag(&self, _: TagName) -> bool {
// I think that spaces can technically have tags, but afaik no client
// exposes them, so we'll just always return false here for now.
false
} }
} }
impl ToString for SpaceItem { impl ToString for SpaceItem {
fn to_string(&self) -> String { fn to_string(&self) -> String {
return self.room.room_id().to_string(); return self.room_id().to_string();
} }
} }
@ -894,7 +967,7 @@ impl ListItem<IambInfo> for SpaceItem {
} }
fn get_word(&self) -> Option<String> { fn get_word(&self) -> Option<String> {
self.room.room_id().to_string().into() self.room_id().to_string().into()
} }
} }
@ -905,7 +978,7 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for SpaceItem {
ctx: &ProgramContext, ctx: &ProgramContext,
_: &mut ProgramStore, _: &mut ProgramStore,
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> { ) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
room_prompt(self.room.room_id(), act, ctx) room_prompt(self.room_id(), act, ctx)
} }
} }
@ -1200,3 +1273,93 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for MemberItem {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use matrix_sdk::ruma::{room_alias_id, server_name};
#[derive(Debug, Eq, PartialEq)]
struct TestRoomItem {
room_id: OwnedRoomId,
tags: Vec<TagName>,
alias: Option<OwnedRoomAliasId>,
name: &'static str,
}
impl RoomLikeItem for &TestRoomItem {
fn room_id(&self) -> &RoomId {
self.room_id.as_ref()
}
fn has_tag(&self, tag: TagName) -> bool {
self.tags.contains(&tag)
}
fn alias(&self) -> Option<&RoomAliasId> {
self.alias.as_deref()
}
fn name(&self) -> &str {
self.name
}
}
#[test]
fn test_sort_rooms() {
let server = server_name!("example.com");
let room1 = TestRoomItem {
room_id: RoomId::new(server).to_owned(),
tags: vec![TagName::Favorite],
alias: Some(room_alias_id!("#room1:example.com").to_owned()),
name: "Z",
};
let room2 = TestRoomItem {
room_id: RoomId::new(server).to_owned(),
tags: vec![],
alias: Some(room_alias_id!("#a:example.com").to_owned()),
name: "Unnamed Room",
};
let room3 = TestRoomItem {
room_id: RoomId::new(server).to_owned(),
tags: vec![],
alias: None,
name: "Cool Room",
};
// 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));
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));
assert_eq!(rooms, vec![&room1, &room2, &room3]);
// Sort by Favorite and Alias before Name to show order matters.
let mut rooms = vec![&room1, &room2, &room3];
let fields = &[
SortColumn(SortFieldRoom::Favorite, SortOrder::Ascending),
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
assert_eq!(rooms, vec![&room1, &room2, &room3]);
// Now flip order of Favorite with Descending
let mut rooms = vec![&room1, &room2, &room3];
let fields = &[
SortColumn(SortFieldRoom::Favorite, SortOrder::Descending),
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
assert_eq!(rooms, vec![&room2, &room3, &room1]);
}
}

View file

@ -22,7 +22,7 @@ use modalkit::{
use crate::base::{IambBufferId, IambInfo, ProgramStore, RoomFocus}; use crate::base::{IambBufferId, IambInfo, ProgramStore, RoomFocus};
use crate::windows::RoomItem; use crate::windows::{room_fields_cmp, RoomItem};
const SPACE_HIERARCHY_DEBOUNCE: Duration = Duration::from_secs(5); const SPACE_HIERARCHY_DEBOUNCE: Duration = Duration::from_secs(5);
@ -120,7 +120,7 @@ impl<'a> StatefulWidget for Space<'a> {
match res { match res {
Ok(members) => { Ok(members) => {
let items = members let mut items = members
.into_iter() .into_iter()
.filter_map(|id| { .filter_map(|id| {
let (room, _, tags) = let (room, _, tags) =
@ -133,7 +133,9 @@ impl<'a> StatefulWidget for Space<'a> {
None None
} }
}) })
.collect(); .collect::<Vec<_>>();
let fields = &self.store.application.settings.tunables.sort.rooms;
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
state.list.set(items); state.list.set(items);
state.last_fetch = Some(Instant::now()); state.last_fetch = Some(Instant::now());

View file

@ -330,34 +330,30 @@ async fn refresh_rooms(client: &Client, store: &AsyncProgramStore) {
for room in client.invited_rooms().into_iter() { for room in client.invited_rooms().into_iter() {
let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string(); let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string();
let tags = room.tags().await.unwrap_or_default();
names.push((room.room_id().to_owned(), name)); names.push((room.room_id().to_owned(), name));
if room.is_direct() { if room.is_direct() {
let tags = room.tags().await.unwrap_or_default();
dms.push(Arc::new((room.into(), tags))); dms.push(Arc::new((room.into(), tags)));
} else if room.is_space() { } else if room.is_space() {
spaces.push(room.into()); spaces.push(Arc::new((room.into(), tags)));
} else { } else {
let tags = room.tags().await.unwrap_or_default();
rooms.push(Arc::new((room.into(), tags))); rooms.push(Arc::new((room.into(), tags)));
} }
} }
for room in client.joined_rooms().into_iter() { for room in client.joined_rooms().into_iter() {
let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string(); let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string();
let tags = room.tags().await.unwrap_or_default();
names.push((room.room_id().to_owned(), name)); names.push((room.room_id().to_owned(), name));
if room.is_direct() { if room.is_direct() {
let tags = room.tags().await.unwrap_or_default();
dms.push(Arc::new((room.into(), tags))); dms.push(Arc::new((room.into(), tags)));
} else if room.is_space() { } else if room.is_space() {
spaces.push(room.into()); spaces.push(Arc::new((room.into(), tags)));
} else { } else {
let tags = room.tags().await.unwrap_or_default();
rooms.push(Arc::new((room.into(), tags))); rooms.push(Arc::new((room.into(), tags)));
} }
} }