diff --git a/README.md b/README.md index dde272d0a..0b8735c55 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,8 @@ two other TUI clients and Element Web: | Pushrules | :x: | :heavy_check_mark: | :x: | :heavy_check_mark: | | Send read markers | :x: ([#11]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Display read markers | :x: ([#11]) | :x: | :x: | :heavy_check_mark: | -| Sending Invites | :x: ([#7]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Accepting Invites | :x: ([#7]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Sending Invites | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Accepting Invites | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Typing Notification | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | E2E | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Replies | :x: ([#3]) | :heavy_check_mark: | :x: | :heavy_check_mark: | diff --git a/src/base.rs b/src/base.rs index a8fbb2d05..13456d591 100644 --- a/src/base.rs +++ b/src/base.rs @@ -72,6 +72,9 @@ pub enum SetRoomField { #[derive(Clone, Debug, Eq, PartialEq)] pub enum RoomAction { + InviteAccept, + InviteReject, + InviteSend(OwnedUserId), Members(Box>), Set(SetRoomField), } @@ -180,7 +183,7 @@ pub type IambResult = UIResult; #[derive(thiserror::Error, Debug)] pub enum IambError { - #[error("Unknown room identifier: {0}")] + #[error("Invalid user identifier: {0}")] InvalidUserId(String), #[error("Invalid verification user/device pair: {0}")] @@ -213,6 +216,9 @@ pub enum IambError { #[error("Current window is not a room")] NoSelectedRoom, + #[error("You do not have a current invitation to this room")] + NotInvited, + #[error("You need to join the room before you can do that")] NotJoined, diff --git a/src/commands.rs b/src/commands.rs index 00317c475..32340154c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,3 +1,7 @@ +use std::convert::TryFrom; + +use matrix_sdk::ruma::OwnedUserId; + use modalkit::{ editing::base::OpenTarget, env::vim::command::{CommandContext, CommandDescription}, @@ -21,6 +25,53 @@ use crate::base::{ type ProgContext = CommandContext; type ProgResult = CommandResult; +fn iamb_invite(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + let args = desc.arg.strings()?; + + if args.is_empty() { + return Err(CommandError::InvalidArgument); + } + + let ract = match args[0].as_str() { + "accept" => { + if args.len() != 1 { + return Err(CommandError::InvalidArgument); + } + + RoomAction::InviteAccept + }, + "reject" => { + if args.len() != 1 { + return Err(CommandError::InvalidArgument); + } + + RoomAction::InviteReject + }, + "send" => { + if args.len() != 2 { + return Err(CommandError::InvalidArgument); + } + + if let Ok(user) = OwnedUserId::try_from(args[1].as_str()) { + RoomAction::InviteSend(user) + } else { + let msg = format!("Invalid user identifier: {}", args[1]); + let err = CommandError::Error(msg); + + return Err(err); + } + }, + _ => { + return Err(CommandError::InvalidArgument); + }, + }; + + let iact = IambAction::from(ract); + let step = CommandStep::Continue(iact.into(), ctx.context.take()); + + return Ok(step); +} + fn iamb_verify(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { let mut args = desc.arg.strings()?; @@ -182,6 +233,7 @@ fn iamb_download(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult fn add_iamb_commands(cmds: &mut ProgramCommands) { cmds.add_command(ProgramCommand { names: vec!["dms".into()], f: iamb_dms }); cmds.add_command(ProgramCommand { names: vec!["download".into()], f: iamb_download }); + cmds.add_command(ProgramCommand { names: vec!["invite".into()], f: iamb_invite }); cmds.add_command(ProgramCommand { names: vec!["join".into()], f: iamb_join }); cmds.add_command(ProgramCommand { names: vec!["members".into()], f: iamb_members }); cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms }); @@ -203,6 +255,7 @@ pub fn setup_commands() -> ProgramCommands { #[cfg(test)] mod tests { use super::*; + use matrix_sdk::ruma::user_id; use modalkit::editing::action::WindowAction; #[test] @@ -315,4 +368,41 @@ mod tests { let res = cmds.input_cmd("set room.topic A B C", ctx.clone()); assert_eq!(res, Err(CommandError::InvalidArgument)); } + + #[test] + fn test_cmd_invite() { + let mut cmds = setup_commands(); + let ctx = ProgramContext::default(); + + let res = cmds.input_cmd("invite accept", ctx.clone()).unwrap(); + let act = IambAction::Room(RoomAction::InviteAccept); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd("invite reject", ctx.clone()).unwrap(); + let act = IambAction::Room(RoomAction::InviteReject); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd("invite send @user:example.com", ctx.clone()).unwrap(); + let act = + IambAction::Room(RoomAction::InviteSend(user_id!("@user:example.com").to_owned())); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd("invite", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd("invite foo", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd("invite accept @user:example.com", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd("invite reject @user:example.com", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd("invite send", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd("invite @user:example.com", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + } } diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 5a356de94..c0c79c4c3 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -343,10 +343,10 @@ impl WindowOps for IambWindow { .render(area, buf, state); }, IambWindow::RoomList(state) => { - let joined = store.application.worker.joined_rooms(); + let joined = store.application.worker.active_rooms(); let mut items = joined .into_iter() - .map(|(id, name)| RoomItem::new(id, name, store)) + .map(|(room, name)| RoomItem::new(room, name, store)) .collect::>(); items.sort(); diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index e745b79fa..9368de760 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -92,6 +92,12 @@ impl ChatState { } } + pub fn refresh_room(&mut self, store: &mut ProgramStore) { + if let Some(room) = store.application.worker.client.get_room(self.id()) { + self.room = room; + } + } + pub async fn message_command( &mut self, act: MessageAction, diff --git a/src/windows/room/mod.rs b/src/windows/room/mod.rs index 84fd6877f..21880854e 100644 --- a/src/windows/room/mod.rs +++ b/src/windows/room/mod.rs @@ -1,11 +1,15 @@ -use matrix_sdk::{room::Room as MatrixRoom, ruma::RoomId, DisplayName}; +use matrix_sdk::{ + room::{Invited, Room as MatrixRoom}, + ruma::RoomId, + DisplayName, +}; use modalkit::tui::{ buffer::Buffer, - layout::Rect, + layout::{Alignment, Rect}, style::{Modifier as StyleModifier, Style}, - text::{Span, Spans}, - widgets::StatefulWidget, + text::{Span, Spans, Text}, + widgets::{Paragraph, StatefulWidget, Widget}, }; use modalkit::{ @@ -93,6 +97,48 @@ impl RoomState { } } + pub fn refresh_room(&mut self, store: &mut ProgramStore) { + match self { + RoomState::Chat(chat) => chat.refresh_room(store), + RoomState::Space(space) => space.refresh_room(store), + } + } + + fn draw_invite( + &self, + invited: Invited, + area: Rect, + buf: &mut Buffer, + store: &mut ProgramStore, + ) { + let inviter = store.application.worker.get_inviter(invited.clone()); + + let name = match invited.canonical_alias() { + Some(alias) => alias.to_string(), + None => format!("{:?}", store.application.get_room_title(self.id())), + }; + + let mut invited = vec![Span::from(format!( + "You have been invited to join {}", + name + ))]; + + if let Ok(Some(inviter)) = &inviter { + invited.push(Span::from(" by ")); + invited.push(store.application.settings.get_user_span(inviter.user_id())); + } + + let l1 = Spans(invited); + let l2 = Spans::from( + "You can run `:invite accept` or `:invite reject` to accept or reject this invitation.", + ); + let text = Text { lines: vec![l1, l2] }; + + Paragraph::new(text).alignment(Alignment::Center).render(area, buf); + + return; + } + pub async fn message_command( &mut self, act: MessageAction, @@ -124,6 +170,33 @@ impl RoomState { store: &mut ProgramStore, ) -> IambResult, ProgramContext)>> { match act { + RoomAction::InviteAccept => { + if let Some(room) = store.application.worker.client.get_invited_room(self.id()) { + room.accept_invitation().await.map_err(IambError::from)?; + + Ok(vec![]) + } else { + Err(IambError::NotInvited.into()) + } + }, + RoomAction::InviteReject => { + if let Some(room) = store.application.worker.client.get_invited_room(self.id()) { + room.reject_invitation().await.map_err(IambError::from)?; + + Ok(vec![]) + } else { + Err(IambError::NotInvited.into()) + } + }, + RoomAction::InviteSend(user) => { + if let Some(room) = store.application.worker.client.get_joined_room(self.id()) { + room.invite_user_by_id(user.as_ref()).await.map_err(IambError::from)?; + + Ok(vec![]) + } else { + Err(IambError::NotJoined.into()) + } + }, RoomAction::Members(mut cmd) => { let width = Count::Exact(30); let act = @@ -234,6 +307,14 @@ impl TerminalCursor for RoomState { impl WindowOps for RoomState { fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) { + if let MatrixRoom::Invited(_) = self.room() { + self.refresh_room(store); + } + + if let MatrixRoom::Invited(invited) = self.room() { + self.draw_invite(invited.clone(), area, buf, store); + } + match self { RoomState::Chat(chat) => chat.draw(area, buf, focused, store), RoomState::Space(space) => { diff --git a/src/windows/room/space.rs b/src/windows/room/space.rs index f03f64ab9..d910c761f 100644 --- a/src/windows/room/space.rs +++ b/src/windows/room/space.rs @@ -31,6 +31,12 @@ impl SpaceState { SpaceState { room_id, room, list } } + pub fn refresh_room(&mut self, store: &mut ProgramStore) { + if let Some(room) = store.application.worker.client.get_room(self.id()) { + self.room = room; + } + } + pub fn room(&self) -> &MatrixRoom { &self.room } @@ -88,7 +94,13 @@ impl<'a> StatefulWidget for Space<'a> { type State = SpaceState; fn render(self, area: Rect, buffer: &mut Buffer, state: &mut Self::State) { - let members = self.store.application.worker.space_members(state.room_id.clone()).unwrap(); + let members = + if let Ok(m) = self.store.application.worker.space_members(state.room_id.clone()) { + m + } else { + return; + }; + let items = members .into_iter() .filter_map(|id| { diff --git a/src/worker.rs b/src/worker.rs index dee3e01bd..c3963a688 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -17,7 +17,7 @@ use matrix_sdk::{ encryption::verification::{SasVerification, Verification}, event_handler::Ctx, reqwest, - room::{Messages, MessagesOptions, Room as MatrixRoom, RoomMember}, + room::{Invited, Messages, MessagesOptions, Room as MatrixRoom, RoomMember}, ruma::{ api::client::{ room::create_room::v3::{Request as CreateRoomRequest, RoomPreset}, @@ -98,13 +98,14 @@ fn oneshot() -> (ClientReply, ClientResponse) { } pub enum WorkerTask { + ActiveRooms(ClientReply>), DirectMessages(ClientReply>), Init(AsyncProgramStore, ClientReply<()>), LoadOlder(OwnedRoomId, Option, u32, ClientReply), Login(LoginStyle, ClientReply>), + GetInviter(Invited, ClientReply>>), GetRoom(OwnedRoomId, ClientReply>), JoinRoom(String, ClientReply>), - JoinedRooms(ClientReply>), Members(OwnedRoomId, ClientReply>>), SpaceMembers(OwnedRoomId, ClientReply>>), Spaces(ClientReply>), @@ -117,6 +118,9 @@ pub enum WorkerTask { impl Debug for WorkerTask { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { match self { + WorkerTask::ActiveRooms(_) => { + f.debug_tuple("WorkerTask::ActiveRooms").field(&format_args!("_")).finish() + }, WorkerTask::DirectMessages(_) => { f.debug_tuple("WorkerTask::DirectMessages") .field(&format_args!("_")) @@ -142,6 +146,9 @@ impl Debug for WorkerTask { .field(&format_args!("_")) .finish() }, + WorkerTask::GetInviter(invite, _) => { + f.debug_tuple("WorkerTask::GetInviter").field(invite).finish() + }, WorkerTask::GetRoom(room_id, _) => { f.debug_tuple("WorkerTask::GetRoom") .field(room_id) @@ -154,9 +161,6 @@ impl Debug for WorkerTask { .field(&format_args!("_")) .finish() }, - WorkerTask::JoinedRooms(_) => { - f.debug_tuple("WorkerTask::JoinedRooms").field(&format_args!("_")).finish() - }, WorkerTask::Members(room_id, _) => { f.debug_tuple("WorkerTask::Members") .field(room_id) @@ -245,6 +249,14 @@ impl Requester { return response.recv(); } + pub fn get_inviter(&self, invite: Invited) -> IambResult> { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::GetInviter(invite, reply)).unwrap(); + + return response.recv(); + } + pub fn get_room(&self, room_id: OwnedRoomId) -> IambResult<(MatrixRoom, DisplayName)> { let (reply, response) = oneshot(); @@ -261,10 +273,10 @@ impl Requester { return response.recv(); } - pub fn joined_rooms(&self) -> Vec<(MatrixRoom, DisplayName)> { + pub fn active_rooms(&self) -> Vec<(MatrixRoom, DisplayName)> { let (reply, response) = oneshot(); - self.tx.send(WorkerTask::JoinedRooms(reply)).unwrap(); + self.tx.send(WorkerTask::ActiveRooms(reply)).unwrap(); return response.recv(); } @@ -406,13 +418,17 @@ impl ClientWorker { assert!(self.initialized); reply.send(self.join_room(room_id).await); }, + WorkerTask::GetInviter(invited, reply) => { + assert!(self.initialized); + reply.send(self.get_inviter(invited).await); + }, WorkerTask::GetRoom(room_id, reply) => { assert!(self.initialized); reply.send(self.get_room(room_id).await); }, - WorkerTask::JoinedRooms(reply) => { + WorkerTask::ActiveRooms(reply) => { assert!(self.initialized); - reply.send(self.joined_rooms().await); + reply.send(self.active_rooms().await); }, WorkerTask::LoadOlder(room_id, fetch_id, limit, reply) => { assert!(self.initialized); @@ -716,6 +732,12 @@ impl ClientWorker { } } + async fn get_inviter(&mut self, invited: Invited) -> IambResult> { + let details = invited.invite_details().await.map_err(IambError::from)?; + + Ok(details.inviter) + } + async fn get_room(&mut self, room_id: OwnedRoomId) -> IambResult<(MatrixRoom, DisplayName)> { if let Some(room) = self.client.get_room(&room_id) { let name = room.display_name().await.map_err(IambError::from)?; @@ -749,33 +771,53 @@ impl ClientWorker { } } - async fn direct_messages(&mut self) -> Vec<(MatrixRoom, DisplayName)> { + async fn direct_messages(&self) -> Vec<(MatrixRoom, DisplayName)> { let mut rooms = vec![]; - for room in self.client.joined_rooms().into_iter() { - if room.is_space() || !room.is_direct() { + for room in self.client.invited_rooms().into_iter() { + if !room.is_direct() { continue; } - if let Ok(name) = room.display_name().await { - rooms.push((MatrixRoom::from(room), name)) + let name = room.display_name().await.unwrap_or(DisplayName::Empty); + + rooms.push((room.into(), name)); + } + + for room in self.client.joined_rooms().into_iter() { + if !room.is_direct() { + continue; } + + let name = room.display_name().await.unwrap_or(DisplayName::Empty); + + rooms.push((room.into(), name)); } return rooms; } - async fn joined_rooms(&mut self) -> Vec<(MatrixRoom, DisplayName)> { + async fn active_rooms(&self) -> Vec<(MatrixRoom, DisplayName)> { let mut rooms = vec![]; - for room in self.client.joined_rooms().into_iter() { + for room in self.client.invited_rooms().into_iter() { if room.is_space() || room.is_direct() { continue; } - if let Ok(name) = room.display_name().await { - rooms.push((MatrixRoom::from(room), name)) + let name = room.display_name().await.unwrap_or(DisplayName::Empty); + + rooms.push((room.into(), name)); + } + + for room in self.client.joined_rooms().into_iter() { + if room.is_space() || room.is_direct() { + continue; } + + let name = room.display_name().await.unwrap_or(DisplayName::Empty); + + rooms.push((room.into(), name)); } return rooms; @@ -857,17 +899,27 @@ impl ClientWorker { Ok(rooms) } - async fn spaces(&mut self) -> Vec<(MatrixRoom, DisplayName)> { + async fn spaces(&self) -> Vec<(MatrixRoom, DisplayName)> { let mut spaces = vec![]; - for room in self.client.joined_rooms().into_iter() { + for room in self.client.invited_rooms().into_iter() { if !room.is_space() { continue; } - if let Ok(name) = room.display_name().await { - spaces.push((MatrixRoom::from(room), name)); + let name = room.display_name().await.unwrap_or(DisplayName::Empty); + + spaces.push((room.into(), name)); + } + + for room in self.client.joined_rooms().into_iter() { + if !room.is_space() { + continue; } + + let name = room.display_name().await.unwrap_or(DisplayName::Empty); + + spaces.push((room.into(), name)); } return spaces;