Skip to content

Commit

Permalink
Support sending and accepting room invitations (Nheko-Reborn#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
ulyssa committed Jan 12, 2023
1 parent b6f4b03 commit 54ce042
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 32 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
8 changes: 7 additions & 1 deletion src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ pub enum SetRoomField {

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RoomAction {
InviteAccept,
InviteReject,
InviteSend(OwnedUserId),
Members(Box<CommandContext<ProgramContext>>),
Set(SetRoomField),
}
Expand Down Expand Up @@ -180,7 +183,7 @@ pub type IambResult<T> = UIResult<T, IambInfo>;

#[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}")]
Expand Down Expand Up @@ -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,

Expand Down
90 changes: 90 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use std::convert::TryFrom;

use matrix_sdk::ruma::OwnedUserId;

use modalkit::{
editing::base::OpenTarget,
env::vim::command::{CommandContext, CommandDescription},
Expand All @@ -21,6 +25,53 @@ use crate::base::{
type ProgContext = CommandContext<ProgramContext>;
type ProgResult = CommandResult<ProgramCommand>;

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()?;

Expand Down Expand Up @@ -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 });
Expand All @@ -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]
Expand Down Expand Up @@ -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));
}
}
4 changes: 2 additions & 2 deletions src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,10 @@ impl WindowOps<IambInfo> 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::<Vec<_>>();
items.sort();

Expand Down
6 changes: 6 additions & 0 deletions src/windows/room/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
89 changes: 85 additions & 4 deletions src/windows/room/mod.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -124,6 +170,33 @@ impl RoomState {
store: &mut ProgramStore,
) -> IambResult<Vec<(Action<IambInfo>, 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 =
Expand Down Expand Up @@ -234,6 +307,14 @@ impl TerminalCursor for RoomState {

impl WindowOps<IambInfo> 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) => {
Expand Down
14 changes: 13 additions & 1 deletion src/windows/room/space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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| {
Expand Down
Loading

0 comments on commit 54ce042

Please sign in to comment.