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
[![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)
[![#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)

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.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RoomField {
@ -811,7 +940,7 @@ fn emoji_map() -> CompletionMap<String, &'static Emoji> {
#[derive(Default)]
pub struct SyncInfo {
/// 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.
pub rooms: Vec<Arc<(MatrixRoom, Option<Tags>)>>,

View file

@ -20,7 +20,7 @@ use modalkit::tui::{
text::Span,
};
use super::base::{IambId, RoomInfo};
use super::base::{IambId, RoomInfo, SortColumn, SortFieldRoom, SortFieldUser, SortOrder};
macro_rules! usage {
( $($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 COLORS: [Color; 13] = [
@ -213,6 +224,15 @@ pub struct 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> {
match (a, b) {
(Some(a), None) => Some(a),
@ -246,6 +266,33 @@ pub enum UserDisplayStyle {
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)]
pub struct TunableValues {
pub log_level: Level,
@ -254,6 +301,7 @@ pub struct TunableValues {
pub read_receipt_send: bool,
pub read_receipt_display: bool,
pub request_timeout: u64,
pub sort: SortValues,
pub typing_notice_send: bool,
pub typing_notice_display: bool,
pub users: UserOverrides,
@ -270,6 +318,8 @@ pub struct Tunables {
pub read_receipt_send: Option<bool>,
pub read_receipt_display: Option<bool>,
pub request_timeout: Option<u64>,
#[serde(default)]
pub sort: SortOverrides,
pub typing_notice_send: Option<bool>,
pub typing_notice_display: Option<bool>,
pub users: Option<UserOverrides>,
@ -289,6 +339,7 @@ impl Tunables {
read_receipt_send: self.read_receipt_send.or(other.read_receipt_send),
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
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_display: self.typing_notice_display.or(other.typing_notice_display),
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_display: self.read_receipt_display.unwrap_or(true),
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_display: self.typing_notice_display.unwrap_or(true),
users: self.users.unwrap_or_default(),
@ -701,6 +753,43 @@ mod tests {
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]
fn test_parse_layout() {
let user = WindowPath::UserId(user_id!("@user:example.com").to_owned());

View file

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

View file

@ -17,7 +17,9 @@ use matrix_sdk::{
ruma::{
events::room::member::MembershipState,
events::tag::{TagName, Tags},
OwnedRoomAliasId,
OwnedRoomId,
RoomAliasId,
RoomId,
},
};
@ -80,6 +82,10 @@ use crate::base::{
ProgramStore,
RoomAction,
SendAction,
SortColumn,
SortFieldRoom,
SortFieldUser,
SortOrder,
};
use self::{room::RoomState, welcome::WelcomeState};
@ -125,42 +131,87 @@ fn selected_text(s: &str, selected: bool) -> Text {
Text::from(selected_span(s, selected))
}
fn room_cmp(a: &MatrixRoom, b: &MatrixRoom) -> Ordering {
let ca1 = a.canonical_alias();
let ca2 = b.canonical_alias();
let ord = match (ca1, ca2) {
/// Sort `Some` to be less than `None` so that list items with values come before those without.
#[inline]
fn some_cmp<T: Ord>(a: Option<T>, b: Option<T>) -> Ordering {
match (a, b) {
(Some(a), Some(b)) => a.cmp(&b),
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(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 {
let (fava, lowa) = a
.as_ref()
.map(|tags| {
(tags.contains_key(&TagName::Favorite), tags.contains_key(&TagName::LowPriority))
})
.unwrap_or((false, false));
fn user_cmp(a: &MemberItem, b: &MemberItem, field: &SortFieldUser) -> Ordering {
let a_id = a.member.user_id();
let b_id = b.member.user_id();
let (favb, lowb) = b
.as_ref()
.map(|tags| {
(tags.contains_key(&TagName::Favorite), tags.contains_key(&TagName::LowPriority))
})
.unwrap_or((false, false));
match field {
SortFieldUser::UserId => a_id.cmp(b_id),
SortFieldUser::LocalPart => a_id.localpart().cmp(b_id.localpart()),
SortFieldUser::Server => a_id.server_name().cmp(b_id.server_name()),
SortFieldUser::PowerLevel => {
// 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.
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.
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) {
@ -190,6 +241,13 @@ fn append_tags<'a>(tags: &'a Tags, spans: &mut Vec<Span<'a>>, style: 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]
fn room_prompt(
room_id: &RoomId,
@ -399,7 +457,8 @@ impl WindowOps<IambInfo> for IambWindow {
.into_iter()
.map(|room_info| DirectItem::new(room_info, store))
.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);
@ -417,8 +476,13 @@ impl WindowOps<IambInfo> for IambWindow {
if need_fetch {
if let Ok(mems) = store.application.worker.members(room_id.clone()) {
let items = mems.into_iter().map(|m| MemberItem::new(m, room_id.clone()));
state.set(items.collect());
let mut items = mems
.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());
}
}
@ -438,7 +502,8 @@ impl WindowOps<IambInfo> for IambWindow {
.into_iter()
.map(|room_info| RoomItem::new(room_info, store))
.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);
@ -449,15 +514,18 @@ impl WindowOps<IambInfo> for IambWindow {
.render(area, buf, state);
},
IambWindow::SpaceList(state) => {
let items = store
let mut items = store
.application
.sync_info
.spaces
.clone()
.into_iter()
.map(|room| SpaceItem::new(room, store));
state.set(items.collect());
state.draw(area, buf, focused, store);
.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));
state.set(items);
List::new(store)
.empty_message("You haven't joined any spaces yet")
@ -662,6 +730,7 @@ impl Window<IambInfo> for IambWindow {
pub struct RoomItem {
room_info: MatrixRoomInfo,
name: String,
alias: Option<OwnedRoomAliasId>,
}
impl RoomItem {
@ -671,13 +740,14 @@ impl RoomItem {
let info = store.application.get_room_info(room_id.to_owned());
let name = info.name.clone().unwrap_or_default();
let alias = room.canonical_alias();
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());
}
RoomItem { room_info, name }
RoomItem { room_info, name, alias }
}
#[inline]
@ -685,34 +755,31 @@ impl RoomItem {
&self.room_info.deref().0
}
#[inline]
fn room_id(&self) -> &RoomId {
self.room().room_id()
}
#[inline]
fn tags(&self) -> &Option<Tags> {
&self.room_info.deref().1
}
}
impl PartialEq for RoomItem {
fn eq(&self, other: &Self) -> bool {
self.room_id() == other.room_id()
}
impl RoomLikeItem for RoomItem {
fn name(&self) -> &str {
self.name.as_str()
}
impl Eq for RoomItem {}
impl Ord for RoomItem {
fn cmp(&self, other: &Self) -> Ordering {
tag_cmp(self.tags(), other.tags()).then_with(|| room_cmp(self.room(), other.room()))
}
fn alias(&self) -> Option<&RoomAliasId> {
self.alias.as_deref()
}
impl PartialOrd for RoomItem {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.cmp(other).into()
fn room_id(&self) -> &RoomId {
self.room().room_id()
}
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 {
room_info: MatrixRoomInfo,
name: String,
alias: Option<OwnedRoomAliasId>,
}
impl DirectItem {
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 alias = room_info.0.canonical_alias();
DirectItem { room_info, name }
DirectItem { room_info, name, alias }
}
#[inline]
@ -771,17 +840,34 @@ impl DirectItem {
&self.room_info.deref().0
}
#[inline]
fn room_id(&self) -> &RoomId {
self.room().room_id()
}
#[inline]
fn tags(&self) -> &Option<Tags> {
&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 {
fn to_string(&self) -> String {
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 {
fn prompt(
&mut self,
@ -840,51 +906,58 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for DirectItem {
#[derive(Clone)]
pub struct SpaceItem {
room: MatrixRoom,
room_info: MatrixRoomInfo,
name: String,
alias: Option<OwnedRoomAliasId>,
}
impl SpaceItem {
fn new(room: MatrixRoom, store: &mut ProgramStore) -> Self {
let room_id = room.room_id();
fn new(room_info: MatrixRoomInfo, store: &mut ProgramStore) -> Self {
let room_id = room_info.0.room_id();
let name = store
.application
.get_room_info(room_id.to_owned())
.name
.clone()
.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());
}
SpaceItem { room, name }
SpaceItem { room_info, name, alias }
}
#[inline]
fn room(&self) -> &MatrixRoom {
&self.room_info.deref().0
}
}
impl PartialEq for SpaceItem {
fn eq(&self, other: &Self) -> bool {
self.room.room_id() == other.room.room_id()
}
impl RoomLikeItem for SpaceItem {
fn name(&self) -> &str {
self.name.as_str()
}
impl Eq for SpaceItem {}
impl Ord for SpaceItem {
fn cmp(&self, other: &Self) -> Ordering {
room_cmp(&self.room, &other.room)
}
fn room_id(&self) -> &RoomId {
self.room().room_id()
}
impl PartialOrd for SpaceItem {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.cmp(other).into()
fn alias(&self) -> Option<&RoomAliasId> {
self.alias.as_deref()
}
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 {
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> {
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,
_: &mut ProgramStore,
) -> 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::windows::RoomItem;
use crate::windows::{room_fields_cmp, RoomItem};
const SPACE_HIERARCHY_DEBOUNCE: Duration = Duration::from_secs(5);
@ -120,7 +120,7 @@ impl<'a> StatefulWidget for Space<'a> {
match res {
Ok(members) => {
let items = members
let mut items = members
.into_iter()
.filter_map(|id| {
let (room, _, tags) =
@ -133,7 +133,9 @@ impl<'a> StatefulWidget for Space<'a> {
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.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() {
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));
if room.is_direct() {
let tags = room.tags().await.unwrap_or_default();
dms.push(Arc::new((room.into(), tags)));
} else if room.is_space() {
spaces.push(room.into());
spaces.push(Arc::new((room.into(), tags)));
} else {
let tags = room.tags().await.unwrap_or_default();
rooms.push(Arc::new((room.into(), tags)));
}
}
for room in client.joined_rooms().into_iter() {
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));
if room.is_direct() {
let tags = room.tags().await.unwrap_or_default();
dms.push(Arc::new((room.into(), tags)));
} else if room.is_space() {
spaces.push(room.into());
spaces.push(Arc::new((room.into(), tags)));
} else {
let tags = room.tags().await.unwrap_or_default();
rooms.push(Arc::new((room.into(), tags)));
}
}