Support sending and accepting room invitations (#7)

This commit is contained in:
Ulyssa 2023-01-11 17:54:49 -08:00
parent b6f4b03c12
commit 54ce042384
No known key found for this signature in database
GPG key ID: 1B3965A3D18B9B64
8 changed files with 279 additions and 32 deletions

View file

@ -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: |

View file

@ -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),
}
@ -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}")]
@ -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,

View file

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

View file

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

View file

@ -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,

View file

@ -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<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 =
@ -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) => {

View file

@ -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| {

View file

@ -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<T>() -> (ClientReply<T>, ClientResponse<T>) {
}
pub enum WorkerTask {
ActiveRooms(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
DirectMessages(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
Init(AsyncProgramStore, ClientReply<()>),
LoadOlder(OwnedRoomId, Option<String>, u32, ClientReply<MessageFetchResult>),
Login(LoginStyle, ClientReply<IambResult<EditInfo>>),
GetInviter(Invited, ClientReply<IambResult<Option<RoomMember>>>),
GetRoom(OwnedRoomId, ClientReply<IambResult<(MatrixRoom, DisplayName)>>),
JoinRoom(String, ClientReply<IambResult<OwnedRoomId>>),
JoinedRooms(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
Members(OwnedRoomId, ClientReply<IambResult<Vec<RoomMember>>>),
SpaceMembers(OwnedRoomId, ClientReply<IambResult<Vec<OwnedRoomId>>>),
Spaces(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
@ -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<Option<RoomMember>> {
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<Option<RoomMember>> {
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.invited_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));
}
for room in self.client.joined_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));
}
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.invited_rooms().into_iter() {
if !room.is_space() {
continue;
}
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;
}
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));
}
return spaces;