Support displaying and editing room tags (#15)

This commit is contained in:
Ulyssa 2023-01-25 17:54:16 -08:00
parent 4f2261e66f
commit a6888bbc93
No known key found for this signature in database
GPG key ID: 1B3965A3D18B9B64
8 changed files with 443 additions and 109 deletions

View file

@ -49,7 +49,7 @@ two other TUI clients and Element Web:
| --------------------------------------- | :---------- | :------: | :--------------: | :-----------------: | | --------------------------------------- | :---------- | :------: | :--------------: | :-----------------: |
| Room directory | ❌ ([#14]) | ❌ | ✔️ | ✔️ | | Room directory | ❌ ([#14]) | ❌ | ✔️ | ✔️ |
| Room tag showing | ❌ ([#15]) | ✔️ | ❌ | ✔️ | | Room tag showing | ❌ ([#15]) | ✔️ | ❌ | ✔️ |
| Room tag editing | ❌ ([#15]) | ✔️ | ❌ | ✔️ | | Room tag editing | ✔️ | ✔️ | ❌ | ✔️ |
| Search joined rooms | ❌ ([#16]) | ✔️ | ❌ | ✔️ | | Search joined rooms | ❌ ([#16]) | ✔️ | ❌ | ✔️ |
| Room user list | ✔️ | ✔️ | ✔️ | ✔️ | | Room user list | ✔️ | ✔️ | ✔️ | ✔️ |
| Display Room Description | ✔️ | ✔️ | ✔️ | ✔️ | | Display Room Description | ✔️ | ✔️ | ✔️ | ✔️ |

View file

@ -8,6 +8,7 @@ use tracing::warn;
use matrix_sdk::{ use matrix_sdk::{
encryption::verification::SasVerification, encryption::verification::SasVerification,
room::Joined,
ruma::{ ruma::{
events::room::message::{ events::room::message::{
OriginalRoomMessageEvent, OriginalRoomMessageEvent,
@ -16,6 +17,7 @@ use matrix_sdk::{
RoomMessageEvent, RoomMessageEvent,
RoomMessageEventContent, RoomMessageEventContent,
}, },
events::tag::{TagName, Tags},
EventId, EventId,
OwnedEventId, OwnedEventId,
OwnedRoomId, OwnedRoomId,
@ -94,9 +96,10 @@ pub enum MessageAction {
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum SetRoomField { pub enum RoomField {
Name(String), Name,
Topic(String), Tag(TagName),
Topic,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -105,13 +108,8 @@ pub enum RoomAction {
InviteReject, InviteReject,
InviteSend(OwnedUserId), InviteSend(OwnedUserId),
Members(Box<CommandContext<ProgramContext>>), Members(Box<CommandContext<ProgramContext>>),
Set(SetRoomField), Set(RoomField, String),
} Unset(RoomField),
impl From<SetRoomField> for RoomAction {
fn from(act: SetRoomField) -> Self {
RoomAction::Set(act)
}
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -194,6 +192,12 @@ impl ApplicationAction for IambAction {
} }
} }
impl From<RoomAction> for ProgramAction {
fn from(act: RoomAction) -> Self {
IambAction::from(act).into()
}
}
impl From<IambAction> for ProgramAction { impl From<IambAction> for ProgramAction {
fn from(act: IambAction) -> Self { fn from(act: IambAction) -> Self {
Action::Application(act) Action::Application(act)
@ -277,6 +281,7 @@ pub enum RoomFetchStatus {
#[derive(Default)] #[derive(Default)]
pub struct RoomInfo { pub struct RoomInfo {
pub name: Option<String>, pub name: Option<String>,
pub tags: Option<Tags>,
pub keys: HashMap<OwnedEventId, MessageKey>, pub keys: HashMap<OwnedEventId, MessageKey>,
pub messages: Messages, pub messages: Messages,
@ -437,6 +442,10 @@ impl ChatStore {
} }
} }
pub fn get_joined_room(&self, room_id: &RoomId) -> Option<Joined> {
self.worker.client.get_joined_room(room_id)
}
pub fn get_room_title(&self, room_id: &RoomId) -> String { pub fn get_room_title(&self, room_id: &RoomId) -> String {
self.rooms self.rooms
.get(room_id) .get(room_id)

View file

@ -1,6 +1,6 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use matrix_sdk::ruma::OwnedUserId; use matrix_sdk::ruma::{events::tag::TagName, OwnedUserId};
use modalkit::{ use modalkit::{
editing::base::OpenTarget, editing::base::OpenTarget,
@ -17,14 +17,38 @@ use crate::base::{
ProgramCommands, ProgramCommands,
ProgramContext, ProgramContext,
RoomAction, RoomAction,
RoomField,
SendAction, SendAction,
SetRoomField,
VerifyAction, VerifyAction,
}; };
type ProgContext = CommandContext<ProgramContext>; type ProgContext = CommandContext<ProgramContext>;
type ProgResult = CommandResult<ProgramCommand>; type ProgResult = CommandResult<ProgramCommand>;
/// Convert strings the user types into a tag name.
fn tag_name(name: String) -> Result<TagName, CommandError> {
let tag = match name.as_str() {
"fav" | "favorite" | "favourite" | "m.favourite" => TagName::Favorite,
"low" | "lowpriority" | "low_priority" | "low-priority" | "m.lowpriority" => {
TagName::LowPriority
},
"servernotice" | "server_notice" | "server-notice" | "m.server_notice" => {
TagName::ServerNotice
},
_ => {
if let Ok(tag) = name.parse() {
TagName::User(tag)
} else {
let msg = format!("Invalid user tag name: {}", name);
return Err(CommandError::Error(msg));
}
},
};
Ok(tag)
}
fn iamb_invite(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { fn iamb_invite(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
let args = desc.arg.strings()?; let args = desc.arg.strings()?;
@ -225,22 +249,46 @@ fn iamb_join(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Ok(step); return Ok(step);
} }
fn iamb_set(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
let mut args = desc.arg.strings()?; let mut args = desc.arg.strings()?;
if args.len() != 2 { if args.len() < 2 {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
} }
let field = args.remove(0); let field = args.remove(0);
let value = args.remove(0); let action = args.remove(0);
let act: IambAction = match field.as_str() { if args.len() > 1 {
"room.name" => RoomAction::Set(SetRoomField::Name(value)).into(),
"room.topic" => RoomAction::Set(SetRoomField::Topic(value)).into(),
_ => {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
}, }
let act: IambAction = match (field.as_str(), action.as_str(), args.pop()) {
// :room name set <room-name>
("name", "set", Some(s)) => RoomAction::Set(RoomField::Name, s).into(),
("name", "set", None) => return Result::Err(CommandError::InvalidArgument),
// :room name unset
("name", "unset", None) => RoomAction::Unset(RoomField::Name).into(),
("name", "unset", Some(_)) => return Result::Err(CommandError::InvalidArgument),
// :room topic set <topic>
("topic", "set", Some(s)) => RoomAction::Set(RoomField::Topic, s).into(),
("topic", "set", None) => return Result::Err(CommandError::InvalidArgument),
// :room topic unset
("topic", "unset", None) => RoomAction::Unset(RoomField::Topic).into(),
("topic", "unset", Some(_)) => return Result::Err(CommandError::InvalidArgument),
// :room tag set <tag-name>
("tag", "set", Some(s)) => RoomAction::Set(RoomField::Tag(tag_name(s)?), "".into()).into(),
("tag", "set", None) => return Result::Err(CommandError::InvalidArgument),
// :room tag unset <tag-name>
("tag", "unset", Some(s)) => RoomAction::Unset(RoomField::Tag(tag_name(s)?)).into(),
("tag", "unset", None) => return Result::Err(CommandError::InvalidArgument),
_ => return Result::Err(CommandError::InvalidArgument),
}; };
let step = CommandStep::Continue(act.into(), ctx.context.take()); let step = CommandStep::Continue(act.into(), ctx.context.take());
@ -287,7 +335,7 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
cmds.add_command(ProgramCommand { names: vec!["redact".into()], f: iamb_redact }); cmds.add_command(ProgramCommand { names: vec!["redact".into()], f: iamb_redact });
cmds.add_command(ProgramCommand { names: vec!["reply".into()], f: iamb_reply }); cmds.add_command(ProgramCommand { names: vec!["reply".into()], f: iamb_reply });
cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms }); cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms });
cmds.add_command(ProgramCommand { names: vec!["set".into()], f: iamb_set }); cmds.add_command(ProgramCommand { names: vec!["room".into()], f: iamb_room });
cmds.add_command(ProgramCommand { names: vec!["spaces".into()], f: iamb_spaces }); cmds.add_command(ProgramCommand { names: vec!["spaces".into()], f: iamb_spaces });
cmds.add_command(ProgramCommand { names: vec!["upload".into()], f: iamb_upload }); cmds.add_command(ProgramCommand { names: vec!["upload".into()], f: iamb_upload });
cmds.add_command(ProgramCommand { names: vec!["verify".into()], f: iamb_verify }); cmds.add_command(ProgramCommand { names: vec!["verify".into()], f: iamb_verify });
@ -376,47 +424,227 @@ mod tests {
} }
#[test] #[test]
fn test_cmd_set() { fn test_cmd_room_invalid() {
let mut cmds = setup_commands();
let ctx = ProgramContext::default();
let res = cmds.input_cmd("room", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
let res = cmds.input_cmd("room foo", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
let res = cmds.input_cmd("room set topic", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
}
#[test]
fn test_cmd_room_topic_set() {
let mut cmds = setup_commands(); let mut cmds = setup_commands();
let ctx = ProgramContext::default(); let ctx = ProgramContext::default();
let res = cmds let res = cmds
.input_cmd("set room.topic \"Lots of fun discussion!\"", ctx.clone()) .input_cmd("room topic set \"Lots of fun discussion!\"", ctx.clone())
.unwrap(); .unwrap();
let act = IambAction::Room(SetRoomField::Topic("Lots of fun discussion!".into()).into()); let act = RoomAction::Set(RoomField::Topic, "Lots of fun discussion!".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]); assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds let res = cmds
.input_cmd("set room.topic The\\ Discussion\\ Room", ctx.clone()) .input_cmd("room topic set The\\ Discussion\\ Room", ctx.clone())
.unwrap(); .unwrap();
let act = IambAction::Room(SetRoomField::Topic("The Discussion Room".into()).into()); let act = RoomAction::Set(RoomField::Topic, "The Discussion Room".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]); assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("set room.topic Development", ctx.clone()).unwrap(); let res = cmds.input_cmd("room topic set Development", ctx.clone()).unwrap();
let act = IambAction::Room(SetRoomField::Topic("Development".into()).into()); let act = RoomAction::Set(RoomField::Topic, "Development".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]); assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("set room.name Development", ctx.clone()).unwrap(); let res = cmds.input_cmd("room topic", ctx.clone());
let act = IambAction::Room(SetRoomField::Name("Development".into()).into()); assert_eq!(res, Err(CommandError::InvalidArgument));
let res = cmds.input_cmd("room topic set", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
let res = cmds.input_cmd("room topic set A B C", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
}
#[test]
fn test_cmd_room_name_invalid() {
let mut cmds = setup_commands();
let ctx = ProgramContext::default();
let res = cmds.input_cmd("room name", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
let res = cmds.input_cmd("room name foo", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
}
#[test]
fn test_cmd_room_name_set() {
let mut cmds = setup_commands();
let ctx = ProgramContext::default();
let res = cmds.input_cmd("room name set Development", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Name, "Development".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]); assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds let res = cmds
.input_cmd("set room.name \"Application Development\"", ctx.clone()) .input_cmd("room name set \"Application Development\"", ctx.clone())
.unwrap(); .unwrap();
let act = IambAction::Room(SetRoomField::Name("Application Development".into()).into()); let act = RoomAction::Set(RoomField::Name, "Application Development".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]); assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("set", ctx.clone()); let res = cmds.input_cmd("room name set", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
}
#[test]
fn test_cmd_room_name_unset() {
let mut cmds = setup_commands();
let ctx = ProgramContext::default();
let res = cmds.input_cmd("room name unset", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Name);
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room name unset foo", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
}
#[test]
fn test_cmd_room_tag_set() {
let mut cmds = setup_commands();
let ctx = ProgramContext::default();
let res = cmds.input_cmd("room tag set favourite", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Tag(TagName::Favorite), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set favorite", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Tag(TagName::Favorite), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set fav", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Tag(TagName::Favorite), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set low_priority", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Tag(TagName::LowPriority), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set low-priority", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Tag(TagName::LowPriority), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set low", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Tag(TagName::LowPriority), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set servernotice", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Tag(TagName::ServerNotice), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set server_notice", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Tag(TagName::ServerNotice), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set server_notice", ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::Tag(TagName::ServerNotice), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set u.custom-tag", ctx.clone()).unwrap();
let act = RoomAction::Set(
RoomField::Tag(TagName::User("u.custom-tag".parse().unwrap())),
"".into(),
);
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag set u.irc", ctx.clone()).unwrap();
let act =
RoomAction::Set(RoomField::Tag(TagName::User("u.irc".parse().unwrap())), "".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument)); assert_eq!(res, Err(CommandError::InvalidArgument));
let res = cmds.input_cmd("set room.name", ctx.clone()); let res = cmds.input_cmd("room tag set", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument)); assert_eq!(res, Err(CommandError::InvalidArgument));
let res = cmds.input_cmd("set room.topic", ctx.clone()); let res = cmds.input_cmd("room tag set unknown", ctx.clone());
assert_eq!(res, Err(CommandError::Error("Invalid user tag name: unknown".into())));
let res = cmds.input_cmd("room tag set needs-leading-u-dot", ctx.clone());
assert_eq!(
res,
Err(CommandError::Error("Invalid user tag name: needs-leading-u-dot".into()))
);
}
#[test]
fn test_cmd_room_tag_unset() {
let mut cmds = setup_commands();
let ctx = ProgramContext::default();
let res = cmds.input_cmd("room tag unset favourite", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::Favorite));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset favorite", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::Favorite));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset fav", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::Favorite));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset low_priority", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::LowPriority));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset low-priority", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::LowPriority));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset low", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::LowPriority));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset servernotice", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::ServerNotice));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset server_notice", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::ServerNotice));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset server_notice", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::ServerNotice));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset u.custom-tag", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::User("u.custom-tag".parse().unwrap())));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag unset u.irc", ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::Tag(TagName::User("u.irc".parse().unwrap())));
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room tag", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument)); assert_eq!(res, Err(CommandError::InvalidArgument));
let res = cmds.input_cmd("set room.topic A B C", ctx.clone()); let res = cmds.input_cmd("room tag set", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument)); assert_eq!(res, Err(CommandError::InvalidArgument));
let res = cmds.input_cmd("room tag unset unknown", ctx.clone());
assert_eq!(res, Err(CommandError::Error("Invalid user tag name: unknown".into())));
let res = cmds.input_cmd("room tag unset needs-leading-u-dot", ctx.clone());
assert_eq!(
res,
Err(CommandError::Error("Invalid user tag name: needs-leading-u-dot".into()))
);
} }
#[test] #[test]

View file

@ -141,6 +141,7 @@ pub fn mock_room() -> RoomInfo {
keys: mock_keys(), keys: mock_keys(),
messages: mock_messages(), messages: mock_messages(),
tags: None,
fetch_id: RoomFetchStatus::NotStarted, fetch_id: RoomFetchStatus::NotStarted,
fetch_last: None, fetch_last: None,

View file

@ -4,7 +4,12 @@ use std::collections::hash_map::Entry;
use matrix_sdk::{ use matrix_sdk::{
encryption::verification::{format_emojis, SasVerification}, encryption::verification::{format_emojis, SasVerification},
room::{Room as MatrixRoom, RoomMember}, room::{Room as MatrixRoom, RoomMember},
ruma::{events::room::member::MembershipState, OwnedRoomId, RoomId}, ruma::{
events::room::member::MembershipState,
events::tag::{TagName, Tags},
OwnedRoomId,
RoomId,
},
DisplayName, DisplayName,
}; };
@ -346,7 +351,7 @@ impl WindowOps<IambInfo> for IambWindow {
let joined = store.application.worker.active_rooms(); let joined = store.application.worker.active_rooms();
let mut items = joined let mut items = joined
.into_iter() .into_iter()
.map(|(room, name)| RoomItem::new(room, name, store)) .map(|(room, name, tags)| RoomItem::new(room, name, tags, store))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
items.sort(); items.sort();
@ -471,8 +476,8 @@ impl Window<IambInfo> for IambWindow {
fn open(id: IambId, store: &mut ProgramStore) -> IambResult<Self> { fn open(id: IambId, store: &mut ProgramStore) -> IambResult<Self> {
match id { match id {
IambId::Room(room_id) => { IambId::Room(room_id) => {
let (room, name) = store.application.worker.get_room(room_id)?; let (room, name, tags) = store.application.worker.get_room(room_id)?;
let room = RoomState::new(room, name, store); let room = RoomState::new(room, name, tags, store);
return Ok(room.into()); return Ok(room.into());
}, },
@ -519,8 +524,8 @@ impl Window<IambInfo> for IambWindow {
let room_id = worker.join_room(v.key().to_string())?; let room_id = worker.join_room(v.key().to_string())?;
v.insert(room_id.clone()); v.insert(room_id.clone());
let (room, name) = store.application.worker.get_room(room_id)?; let (room, name, tags) = store.application.worker.get_room(room_id)?;
let room = RoomState::new(room, name, store); let room = RoomState::new(room, name, tags, store);
Ok(room.into()) Ok(room.into())
}, },
@ -547,16 +552,24 @@ impl Window<IambInfo> for IambWindow {
#[derive(Clone)] #[derive(Clone)]
pub struct RoomItem { pub struct RoomItem {
room: MatrixRoom, room: MatrixRoom,
tags: Option<Tags>,
name: String, name: String,
} }
impl RoomItem { impl RoomItem {
fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self { fn new(
room: MatrixRoom,
name: DisplayName,
tags: Option<Tags>,
store: &mut ProgramStore,
) -> Self {
let name = name.to_string(); let name = name.to_string();
store.application.set_room_name(room.room_id(), name.as_str()); let info = store.application.get_room_info(room.room_id().to_owned());
info.name = name.clone().into();
info.tags = tags.clone();
RoomItem { room, name } RoomItem { room, tags, name }
} }
} }
@ -570,7 +583,29 @@ impl Eq for RoomItem {}
impl Ord for RoomItem { impl Ord for RoomItem {
fn cmp(&self, other: &Self) -> Ordering { fn cmp(&self, other: &Self) -> Ordering {
room_cmp(&self.room, &other.room) let (fava, lowa) = self
.tags
.as_ref()
.map(|tags| {
(tags.contains_key(&TagName::Favorite), tags.contains_key(&TagName::LowPriority))
})
.unwrap_or((false, false));
let (favb, lowb) = other
.tags
.as_ref()
.map(|tags| {
(tags.contains_key(&TagName::Favorite), tags.contains_key(&TagName::LowPriority))
})
.unwrap_or((false, false));
// If self has Favorite and other doesn't, it should sort earlier in room list.
let cmpf = favb.cmp(&fava);
// If self has LowPriority and other doesn't, it should sort later in room list.
let cmpl = lowa.cmp(&lowb);
cmpl.then(cmpf).then_with(|| room_cmp(&self.room, &other.room))
} }
} }
@ -588,8 +623,40 @@ impl ToString for RoomItem {
impl ListItem<IambInfo> for RoomItem { impl ListItem<IambInfo> for RoomItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text { fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
if let Some(tags) = &self.tags {
let style = selected_style(selected);
let mut spans = vec![Span::styled(self.name.as_str(), style)];
if tags.is_empty() {
return Text::from(Spans(spans));
}
spans.push(Span::styled(" (", style));
for (i, tag) in tags.keys().enumerate() {
if i > 0 {
spans.push(Span::styled(", ", style));
}
match tag {
TagName::Favorite => spans.push(Span::styled("Favorite", style)),
TagName::LowPriority => spans.push(Span::styled("Low Priority", style)),
TagName::ServerNotice => spans.push(Span::styled("Server Notice", style)),
TagName::User(tag) => {
spans.push(Span::styled("User Tag: ", style));
spans.push(Span::styled(tag.as_ref(), style));
},
tag => spans.push(Span::styled(format!("{:?}", tag), style)),
}
}
spans.push(Span::styled(")", style));
Text::from(Spans(spans))
} else {
selected_text(self.name.as_str(), selected) selected_text(self.name.as_str(), selected)
} }
}
fn get_word(&self) -> Option<String> { fn get_word(&self) -> Option<String> {
self.room.room_id().to_string().into() self.room.room_id().to_string().into()

View file

@ -1,6 +1,12 @@
use matrix_sdk::{ use matrix_sdk::{
room::{Invited, Room as MatrixRoom}, room::{Invited, Room as MatrixRoom},
ruma::RoomId, ruma::{
events::{
room::{name::RoomNameEventContent, topic::RoomTopicEventContent},
tag::{TagInfo, Tags},
},
RoomId,
},
DisplayName, DisplayName,
}; };
@ -23,6 +29,7 @@ use modalkit::{
PromptAction, PromptAction,
Promptable, Promptable,
Scrollable, Scrollable,
UIError,
}, },
editing::base::{ editing::base::{
Axis, Axis,
@ -48,6 +55,7 @@ use crate::base::{
ProgramContext, ProgramContext,
ProgramStore, ProgramStore,
RoomAction, RoomAction,
RoomField,
SendAction, SendAction,
}; };
@ -85,10 +93,16 @@ impl From<SpaceState> for RoomState {
} }
impl RoomState { impl RoomState {
pub fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self { pub fn new(
room: MatrixRoom,
name: DisplayName,
tags: Option<Tags>,
store: &mut ProgramStore,
) -> Self {
let room_id = room.room_id().to_owned(); let room_id = room.room_id().to_owned();
let info = store.application.get_room_info(room_id); let info = store.application.get_room_info(room_id);
info.name = name.to_string().into(); info.name = name.to_string().into();
info.tags = tags;
if room.is_space() { if room.is_space() {
SpaceState::new(room).into() SpaceState::new(room).into()
@ -207,8 +221,50 @@ impl RoomState {
Ok(vec![(act, cmd.context.take())]) Ok(vec![(act, cmd.context.take())])
}, },
RoomAction::Set(field) => { RoomAction::Set(field, value) => {
store.application.worker.set_room(self.id().to_owned(), field)?; let room = store
.application
.get_joined_room(self.id())
.ok_or(UIError::Application(IambError::NotJoined))?;
match field {
RoomField::Name => {
let ev = RoomNameEventContent::new(value.into());
let _ = room.send_state_event(ev).await.map_err(IambError::from)?;
},
RoomField::Tag(tag) => {
let mut info = TagInfo::new();
info.order = Some(1.0);
let _ = room.set_tag(tag, info).await.map_err(IambError::from)?;
},
RoomField::Topic => {
let ev = RoomTopicEventContent::new(value);
let _ = room.send_state_event(ev).await.map_err(IambError::from)?;
},
}
Ok(vec![])
},
RoomAction::Unset(field) => {
let room = store
.application
.get_joined_room(self.id())
.ok_or(UIError::Application(IambError::NotJoined))?;
match field {
RoomField::Name => {
let ev = RoomNameEventContent::new(None);
let _ = room.send_state_event(ev).await.map_err(IambError::from)?;
},
RoomField::Tag(tag) => {
let _ = room.remove_tag(tag).await.map_err(IambError::from)?;
},
RoomField::Topic => {
let ev = RoomTopicEventContent::new("".into());
let _ = room.send_state_event(ev).await.map_err(IambError::from)?;
},
}
Ok(vec![]) Ok(vec![])
}, },

View file

@ -104,10 +104,10 @@ impl<'a> StatefulWidget for Space<'a> {
let items = members let items = members
.into_iter() .into_iter()
.filter_map(|id| { .filter_map(|id| {
let (room, name) = self.store.application.worker.get_room(id.clone()).ok()?; let (room, name, tags) = self.store.application.worker.get_room(id.clone()).ok()?;
if id != state.room_id { if id != state.room_id {
Some(RoomItem::new(room, name, self.store)) Some(RoomItem::new(room, name, tags, self.store))
} else { } else {
None None
} }

View file

@ -36,8 +36,8 @@ use matrix_sdk::{
message::{MessageType, RoomMessageEventContent}, message::{MessageType, RoomMessageEventContent},
name::RoomNameEventContent, name::RoomNameEventContent,
redaction::{OriginalSyncRoomRedactionEvent, SyncRoomRedactionEvent}, redaction::{OriginalSyncRoomRedactionEvent, SyncRoomRedactionEvent},
topic::RoomTopicEventContent,
}, },
tag::Tags,
typing::SyncTypingEvent, typing::SyncTypingEvent,
AnyMessageLikeEvent, AnyMessageLikeEvent,
AnyTimelineEvent, AnyTimelineEvent,
@ -57,7 +57,7 @@ use matrix_sdk::{
use modalkit::editing::action::{EditInfo, InfoMessage, UIError}; use modalkit::editing::action::{EditInfo, InfoMessage, UIError};
use crate::{ use crate::{
base::{AsyncProgramStore, IambError, IambResult, SetRoomField, VerifyAction}, base::{AsyncProgramStore, IambError, IambResult, VerifyAction},
message::MessageFetchResult, message::MessageFetchResult,
ApplicationSettings, ApplicationSettings,
}; };
@ -100,18 +100,17 @@ fn oneshot<T>() -> (ClientReply<T>, ClientResponse<T>) {
} }
pub enum WorkerTask { pub enum WorkerTask {
ActiveRooms(ClientReply<Vec<(MatrixRoom, DisplayName)>>), ActiveRooms(ClientReply<Vec<(MatrixRoom, DisplayName, Option<Tags>)>>),
DirectMessages(ClientReply<Vec<(MatrixRoom, DisplayName)>>), DirectMessages(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
Init(AsyncProgramStore, ClientReply<()>), Init(AsyncProgramStore, ClientReply<()>),
LoadOlder(OwnedRoomId, Option<String>, u32, ClientReply<MessageFetchResult>), LoadOlder(OwnedRoomId, Option<String>, u32, ClientReply<MessageFetchResult>),
Login(LoginStyle, ClientReply<IambResult<EditInfo>>), Login(LoginStyle, ClientReply<IambResult<EditInfo>>),
GetInviter(Invited, ClientReply<IambResult<Option<RoomMember>>>), GetInviter(Invited, ClientReply<IambResult<Option<RoomMember>>>),
GetRoom(OwnedRoomId, ClientReply<IambResult<(MatrixRoom, DisplayName)>>), GetRoom(OwnedRoomId, ClientReply<IambResult<(MatrixRoom, DisplayName, Option<Tags>)>>),
JoinRoom(String, ClientReply<IambResult<OwnedRoomId>>), JoinRoom(String, ClientReply<IambResult<OwnedRoomId>>),
Members(OwnedRoomId, ClientReply<IambResult<Vec<RoomMember>>>), Members(OwnedRoomId, ClientReply<IambResult<Vec<RoomMember>>>),
SpaceMembers(OwnedRoomId, ClientReply<IambResult<Vec<OwnedRoomId>>>), SpaceMembers(OwnedRoomId, ClientReply<IambResult<Vec<OwnedRoomId>>>),
Spaces(ClientReply<Vec<(MatrixRoom, DisplayName)>>), Spaces(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
SetRoom(OwnedRoomId, SetRoomField, ClientReply<IambResult<()>>),
TypingNotice(OwnedRoomId), TypingNotice(OwnedRoomId),
Verify(VerifyAction, SasVerification, ClientReply<IambResult<EditInfo>>), Verify(VerifyAction, SasVerification, ClientReply<IambResult<EditInfo>>),
VerifyRequest(OwnedUserId, ClientReply<IambResult<EditInfo>>), VerifyRequest(OwnedUserId, ClientReply<IambResult<EditInfo>>),
@ -178,13 +177,6 @@ impl Debug for WorkerTask {
WorkerTask::Spaces(_) => { WorkerTask::Spaces(_) => {
f.debug_tuple("WorkerTask::Spaces").field(&format_args!("_")).finish() f.debug_tuple("WorkerTask::Spaces").field(&format_args!("_")).finish()
}, },
WorkerTask::SetRoom(room_id, field, _) => {
f.debug_tuple("WorkerTask::SetRoom")
.field(room_id)
.field(field)
.field(&format_args!("_"))
.finish()
},
WorkerTask::TypingNotice(room_id) => { WorkerTask::TypingNotice(room_id) => {
f.debug_tuple("WorkerTask::TypingNotice").field(room_id).finish() f.debug_tuple("WorkerTask::TypingNotice").field(room_id).finish()
}, },
@ -259,7 +251,10 @@ impl Requester {
return response.recv(); return response.recv();
} }
pub fn get_room(&self, room_id: OwnedRoomId) -> IambResult<(MatrixRoom, DisplayName)> { pub fn get_room(
&self,
room_id: OwnedRoomId,
) -> IambResult<(MatrixRoom, DisplayName, Option<Tags>)> {
let (reply, response) = oneshot(); let (reply, response) = oneshot();
self.tx.send(WorkerTask::GetRoom(room_id, reply)).unwrap(); self.tx.send(WorkerTask::GetRoom(room_id, reply)).unwrap();
@ -275,7 +270,7 @@ impl Requester {
return response.recv(); return response.recv();
} }
pub fn active_rooms(&self) -> Vec<(MatrixRoom, DisplayName)> { pub fn active_rooms(&self) -> Vec<(MatrixRoom, DisplayName, Option<Tags>)> {
let (reply, response) = oneshot(); let (reply, response) = oneshot();
self.tx.send(WorkerTask::ActiveRooms(reply)).unwrap(); self.tx.send(WorkerTask::ActiveRooms(reply)).unwrap();
@ -299,14 +294,6 @@ impl Requester {
return response.recv(); return response.recv();
} }
pub fn set_room(&self, room_id: OwnedRoomId, ev: SetRoomField) -> IambResult<()> {
let (reply, response) = oneshot();
self.tx.send(WorkerTask::SetRoom(room_id, ev, reply)).unwrap();
return response.recv();
}
pub fn spaces(&self) -> Vec<(MatrixRoom, DisplayName)> { pub fn spaces(&self) -> Vec<(MatrixRoom, DisplayName)> {
let (reply, response) = oneshot(); let (reply, response) = oneshot();
@ -444,10 +431,6 @@ impl ClientWorker {
assert!(self.initialized); assert!(self.initialized);
reply.send(self.members(room_id).await); reply.send(self.members(room_id).await);
}, },
WorkerTask::SetRoom(room_id, field, reply) => {
assert!(self.initialized);
reply.send(self.set_room(room_id, field).await);
},
WorkerTask::SpaceMembers(space, reply) => { WorkerTask::SpaceMembers(space, reply) => {
assert!(self.initialized); assert!(self.initialized);
reply.send(self.space_members(space).await); reply.send(self.space_members(space).await);
@ -721,10 +704,15 @@ impl ClientWorker {
Ok(Some(InfoMessage::from("Successfully logged in!"))) Ok(Some(InfoMessage::from("Successfully logged in!")))
} }
async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<(MatrixRoom, DisplayName)> { async fn direct_message(
&mut self,
user: OwnedUserId,
) -> IambResult<(MatrixRoom, DisplayName, Option<Tags>)> {
for (room, name) in self.direct_messages().await { for (room, name) in self.direct_messages().await {
if room.get_member(user.as_ref()).await.map_err(IambError::from)?.is_some() { if room.get_member(user.as_ref()).await.map_err(IambError::from)?.is_some() {
return Ok((room, name)); let tags = room.tags().await.map_err(IambError::from)?;
return Ok((room, name, tags));
} }
} }
@ -758,11 +746,15 @@ impl ClientWorker {
Ok(details.inviter) Ok(details.inviter)
} }
async fn get_room(&mut self, room_id: OwnedRoomId) -> IambResult<(MatrixRoom, DisplayName)> { async fn get_room(
&mut self,
room_id: OwnedRoomId,
) -> IambResult<(MatrixRoom, DisplayName, Option<Tags>)> {
if let Some(room) = self.client.get_room(&room_id) { if let Some(room) = self.client.get_room(&room_id) {
let name = room.display_name().await.map_err(IambError::from)?; let name = room.display_name().await.map_err(IambError::from)?;
let tags = room.tags().await.map_err(IambError::from)?;
Ok((room, name)) Ok((room, name, tags))
} else { } else {
Err(IambError::UnknownRoom(room_id).into()) Err(IambError::UnknownRoom(room_id).into())
} }
@ -817,7 +809,7 @@ impl ClientWorker {
return rooms; return rooms;
} }
async fn active_rooms(&self) -> Vec<(MatrixRoom, DisplayName)> { async fn active_rooms(&self) -> Vec<(MatrixRoom, DisplayName, Option<Tags>)> {
let mut rooms = vec![]; let mut rooms = vec![];
for room in self.client.invited_rooms().into_iter() { for room in self.client.invited_rooms().into_iter() {
@ -826,8 +818,9 @@ impl ClientWorker {
} }
let name = room.display_name().await.unwrap_or(DisplayName::Empty); let name = room.display_name().await.unwrap_or(DisplayName::Empty);
let tags = room.tags().await.unwrap_or_default();
rooms.push((room.into(), name)); rooms.push((room.into(), name, tags));
} }
for room in self.client.joined_rooms().into_iter() { for room in self.client.joined_rooms().into_iter() {
@ -836,8 +829,9 @@ impl ClientWorker {
} }
let name = room.display_name().await.unwrap_or(DisplayName::Empty); let name = room.display_name().await.unwrap_or(DisplayName::Empty);
let tags = room.tags().await.unwrap_or_default();
rooms.push((room.into(), name)); rooms.push((room.into(), name, tags));
} }
return rooms; return rooms;
@ -886,27 +880,6 @@ impl ClientWorker {
} }
} }
async fn set_room(&mut self, room_id: OwnedRoomId, field: SetRoomField) -> IambResult<()> {
let room = if let Some(r) = self.client.get_joined_room(&room_id) {
r
} else {
return Err(IambError::UnknownRoom(room_id).into());
};
match field {
SetRoomField::Name(name) => {
let ev = RoomNameEventContent::new(name.into());
let _ = room.send_state_event(ev).await.map_err(IambError::from)?;
},
SetRoomField::Topic(topic) => {
let ev = RoomTopicEventContent::new(topic);
let _ = room.send_state_event(ev).await.map_err(IambError::from)?;
},
}
Ok(())
}
async fn space_members(&mut self, space: OwnedRoomId) -> IambResult<Vec<OwnedRoomId>> { async fn space_members(&mut self, space: OwnedRoomId) -> IambResult<Vec<OwnedRoomId>> {
let mut req = SpaceHierarchyRequest::new(&space); let mut req = SpaceHierarchyRequest::new(&space);
req.limit = Some(1000u32.into()); req.limit = Some(1000u32.into());