Skip to content

Commit

Permalink
Support custom sorting for room and user lists (#170)
Browse files Browse the repository at this point in the history
  • Loading branch information
ulyssa authored Oct 21, 2023
1 parent 443ad24 commit 8943909
Show file tree
Hide file tree
Showing 7 changed files with 497 additions and 116 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
131 changes: 130 additions & 1 deletion src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>)>>,
Expand Down
91 changes: 90 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)* ) => {
Expand All @@ -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] = [
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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>,
Expand All @@ -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),
Expand All @@ -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(),
Expand Down Expand Up @@ -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());
Expand Down
2 changes: 2 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::{
ApplicationSettings,
DirectoryValues,
ProfileConfig,
SortOverrides,
TunableValues,
UserColor,
UserDisplayStyle,
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 8943909

Please sign in to comment.