Support adding rooms to spaces (#407)

This commit is contained in:
VAWVAW 2025-05-15 03:26:35 +00:00 committed by GitHub
parent 7dd09e32a8
commit 3e45ca3d2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 366 additions and 5 deletions

View file

@ -177,6 +177,19 @@ pub enum MessageAction {
Unreact(Option<String>, bool),
}
/// An action taken in the currently selected space.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SpaceAction {
/// Add a room or update metadata.
///
/// The [`Option<String>`] argument is the order parameter.
/// The [`bool`] argument indicates whether the room is suggested.
SetChild(OwnedRoomId, Option<String>, bool),
/// Remove the selected room.
RemoveChild,
}
/// The type of room being created.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CreateRoomType {
@ -379,6 +392,9 @@ pub enum RoomField {
/// The room name.
Name,
/// The room id.
Id,
/// A room tag.
Tag(TagName),
@ -497,6 +513,9 @@ pub enum IambAction {
/// Perform an action on the currently selected message.
Message(MessageAction),
/// Perform an action on the current space.
Space(SpaceAction),
/// Open a URL.
OpenLink(String),
@ -538,6 +557,12 @@ impl From<MessageAction> for IambAction {
}
}
impl From<SpaceAction> for IambAction {
fn from(act: SpaceAction) -> Self {
IambAction::Space(act)
}
}
impl From<RoomAction> for IambAction {
fn from(act: RoomAction) -> Self {
IambAction::Room(act)
@ -557,6 +582,7 @@ impl ApplicationAction for IambAction {
IambAction::Homeserver(..) => SequenceStatus::Break,
IambAction::Keys(..) => SequenceStatus::Break,
IambAction::Message(..) => SequenceStatus::Break,
IambAction::Space(..) => SequenceStatus::Break,
IambAction::Room(..) => SequenceStatus::Break,
IambAction::OpenLink(..) => SequenceStatus::Break,
IambAction::Send(..) => SequenceStatus::Break,
@ -572,6 +598,7 @@ impl ApplicationAction for IambAction {
IambAction::Homeserver(..) => SequenceStatus::Atom,
IambAction::Keys(..) => SequenceStatus::Atom,
IambAction::Message(..) => SequenceStatus::Atom,
IambAction::Space(..) => SequenceStatus::Atom,
IambAction::OpenLink(..) => SequenceStatus::Atom,
IambAction::Room(..) => SequenceStatus::Atom,
IambAction::Send(..) => SequenceStatus::Atom,
@ -587,6 +614,7 @@ impl ApplicationAction for IambAction {
IambAction::Homeserver(..) => SequenceStatus::Ignore,
IambAction::Keys(..) => SequenceStatus::Ignore,
IambAction::Message(..) => SequenceStatus::Ignore,
IambAction::Space(..) => SequenceStatus::Ignore,
IambAction::Room(..) => SequenceStatus::Ignore,
IambAction::OpenLink(..) => SequenceStatus::Ignore,
IambAction::Send(..) => SequenceStatus::Ignore,
@ -601,6 +629,7 @@ impl ApplicationAction for IambAction {
IambAction::ClearUnreads => false,
IambAction::Homeserver(..) => false,
IambAction::Message(..) => false,
IambAction::Space(..) => false,
IambAction::Room(..) => false,
IambAction::Keys(..) => false,
IambAction::Send(..) => false,
@ -618,6 +647,12 @@ impl From<RoomAction> for ProgramAction {
}
}
impl From<SpaceAction> for ProgramAction {
fn from(act: SpaceAction) -> Self {
IambAction::from(act).into()
}
}
impl From<IambAction> for ProgramAction {
fn from(act: IambAction) -> Self {
Action::Application(act)
@ -713,10 +748,22 @@ pub enum IambError {
#[error("Current window is not a room or space")]
NoSelectedRoomOrSpace,
/// A failure due to not having a room or space item selected in a list.
#[error("No room or space currently selected in list")]
NoSelectedRoomOrSpaceItem,
/// A failure due to not having a room selected.
#[error("Current window is not a room")]
NoSelectedRoom,
/// A failure due to not having a space selected.
#[error("Current window is not a space")]
NoSelectedSpace,
/// A failure due to not having sufficient permission to perform an action in a room.
#[error("You do not have the permission to do that")]
InsufficientPermission,
/// A failure due to not having an outstanding room invitation.
#[error("You do not have a current invitation to this room")]
NotInvited,

View file

@ -2,9 +2,9 @@
//!
//! The command-bar commands are set up here, and iamb-specific commands are defined here. See
//! [modalkit::env::vim::command] for additional Vim commands we pull in.
use std::convert::TryFrom;
use std::{convert::TryFrom, str::FromStr as _};
use matrix_sdk::ruma::{events::tag::TagName, OwnedUserId};
use matrix_sdk::ruma::{events::tag::TagName, OwnedRoomId, OwnedUserId};
use modalkit::{
commands::{CommandError, CommandResult, CommandStep},
@ -27,6 +27,7 @@ use crate::base::{
RoomAction,
RoomField,
SendAction,
SpaceAction,
VerifyAction,
};
@ -535,6 +536,90 @@ fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Result::Err(CommandError::InvalidArgument)
},
// :room id show
("id", "show", None) => RoomAction::Show(RoomField::Id).into(),
("id", "show", Some(_)) => return Result::Err(CommandError::InvalidArgument),
_ => return Result::Err(CommandError::InvalidArgument),
};
let step = CommandStep::Continue(act.into(), ctx.context.clone());
return Ok(step);
}
fn iamb_space(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
let mut args = desc.arg.options()?;
if args.len() < 2 {
return Err(CommandError::InvalidArgument);
}
let OptionType::Positional(field) = args.remove(0) else {
return Err(CommandError::InvalidArgument);
};
let OptionType::Positional(action) = args.remove(0) else {
return Err(CommandError::InvalidArgument);
};
let act: IambAction = match (field.as_str(), action.as_str()) {
("child", "remove") => {
if !(args.is_empty()) {
return Err(CommandError::InvalidArgument);
}
SpaceAction::RemoveChild.into()
},
// :space child set
("child", "set") => {
let mut order = None;
let mut suggested = false;
let mut raw_child = None;
for arg in args {
match arg {
OptionType::Flag(name, Some(arg)) => {
match name.as_str() {
"order" => {
if order.is_some() {
let msg = "Multiple ++order arguments are not allowed";
let err = CommandError::Error(msg.into());
return Err(err);
} else {
order = Some(arg);
}
},
_ => return Err(CommandError::InvalidArgument),
}
},
OptionType::Flag(name, None) => {
match name.as_str() {
"suggested" => suggested = true,
_ => return Err(CommandError::InvalidArgument),
}
},
OptionType::Positional(arg) => {
if raw_child.is_some() {
let msg = "Multiple room arguments are not allowed";
let err = CommandError::Error(msg.into());
return Err(err);
}
raw_child = Some(arg);
},
}
}
let child = if let Some(child) = raw_child {
OwnedRoomId::from_str(&child)
.map_err(|_| CommandError::Error("Invalid room id specified".into()))?
} else {
let msg = "Must specify a room to add";
return Err(CommandError::Error(msg.into()));
};
SpaceAction::SetChild(child, order, suggested).into()
},
_ => return Result::Err(CommandError::InvalidArgument),
};
@ -671,6 +756,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
f: iamb_rooms,
});
cmds.add_command(ProgramCommand { name: "room".into(), aliases: vec![], f: iamb_room });
cmds.add_command(ProgramCommand {
name: "space".into(),
aliases: vec![],
f: iamb_space,
});
cmds.add_command(ProgramCommand {
name: "spaces".into(),
aliases: vec![],
@ -725,7 +815,7 @@ pub fn setup_commands() -> ProgramCommands {
#[cfg(test)]
mod tests {
use super::*;
use matrix_sdk::ruma::user_id;
use matrix_sdk::ruma::{room_id, user_id};
use modalkit::actions::WindowAction;
use modalkit::editing::context::EditContext;
@ -1067,6 +1157,103 @@ mod tests {
assert_eq!(res, vec![(act.into(), ctx.clone())]);
}
#[test]
fn test_cmd_room_id_show() {
let mut cmds = setup_commands();
let ctx = EditContext::default();
let res = cmds.input_cmd("room id show", ctx.clone()).unwrap();
let act = RoomAction::Show(RoomField::Id);
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let res = cmds.input_cmd("room id show foo", ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
}
#[test]
fn test_cmd_space_child() {
let mut cmds = setup_commands();
let ctx = EditContext::default();
let cmd = "space";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
let cmd = "space ++foo bar baz";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
let cmd = "space child foo";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
}
#[test]
fn test_cmd_space_child_set() {
let mut cmds = setup_commands();
let ctx = EditContext::default();
let cmd = "space child set !roomid:example.org";
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
let act = SpaceAction::SetChild(room_id!("!roomid:example.org").to_owned(), None, false);
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let cmd = "space child set ++order=abcd ++suggested !roomid:example.org";
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
let act = SpaceAction::SetChild(
room_id!("!roomid:example.org").to_owned(),
Some("abcd".into()),
true,
);
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let cmd = "space child set ++order=abcd ++order=1234 !roomid:example.org";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(
res,
Err(CommandError::Error("Multiple ++order arguments are not allowed".into()))
);
let cmd = "space child set !roomid:example.org !otherroom:example.org";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::Error("Multiple room arguments are not allowed".into())));
let cmd = "space child set ++foo=abcd !roomid:example.org";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
let cmd = "space child set ++foo !roomid:example.org";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
let cmd = "space child ++order=abcd ++suggested set !roomid:example.org";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
let cmd = "space child set foo";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::Error("Invalid room id specified".into())));
let cmd = "space child set";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::Error("Must specify a room to add".into())));
}
#[test]
fn test_cmd_space_child_remove() {
let mut cmds = setup_commands();
let ctx = EditContext::default();
let cmd = "space child remove";
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
let act = SpaceAction::RemoveChild;
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let cmd = "space child remove foo";
let res = cmds.input_cmd(cmd, ctx.clone());
assert_eq!(res, Err(CommandError::InvalidArgument));
}
#[test]
fn test_cmd_invite() {
let mut cmds = setup_commands();

View file

@ -557,6 +557,9 @@ impl Application {
IambAction::Message(act) => {
self.screen.current_window_mut()?.message_command(act, ctx, store).await?
},
IambAction::Space(act) => {
self.screen.current_window_mut()?.space_command(act, ctx, store).await?
},
IambAction::Room(act) => {
let acts = self.screen.current_window_mut()?.room_command(act, ctx, store).await?;
self.action_prepend(acts);

View file

@ -76,6 +76,7 @@ use crate::base::{
SortFieldRoom,
SortFieldUser,
SortOrder,
SpaceAction,
UnreadInfo,
};
@ -360,6 +361,19 @@ impl IambWindow {
}
}
pub async fn space_command(
&mut self,
act: SpaceAction,
ctx: ProgramContext,
store: &mut ProgramStore,
) -> IambResult<EditInfo> {
if let IambWindow::Room(w) = self {
w.space_command(act, ctx, store).await
} else {
return Err(IambError::NoSelectedRoom.into());
}
}
pub async fn room_command(
&mut self,
act: RoomAction,

View file

@ -66,6 +66,7 @@ use crate::base::{
RoomAction,
RoomField,
SendAction,
SpaceAction,
};
use self::chat::ChatState;
@ -214,6 +215,18 @@ impl RoomState {
}
}
pub async fn space_command(
&mut self,
act: SpaceAction,
ctx: ProgramContext,
store: &mut ProgramStore,
) -> IambResult<EditInfo> {
match self {
RoomState::Space(space) => space.space_command(act, ctx, store).await,
RoomState::Chat(_) => Err(IambError::NoSelectedSpace.into()),
}
}
pub async fn send_command(
&mut self,
act: SendAction,
@ -464,6 +477,9 @@ impl RoomState {
RoomField::Aliases => {
// This never happens, aliases is only used for showing
},
RoomField::Id => {
// This never happens, id is only used for showing
},
}
Ok(vec![])
@ -559,6 +575,9 @@ impl RoomState {
RoomField::Aliases => {
// This will not happen, you cannot unset all aliases
},
RoomField::Id => {
// This never happens, id is only used for showing
},
}
Ok(vec![])
@ -574,6 +593,10 @@ impl RoomState {
let visibility = room.history_visibility();
format!("Room history visibility: {visibility}")
},
RoomField::Id => {
let id = room.room_id();
format!("Room identifier: {id}")
},
RoomField::Name => {
match room.name() {
None => "Room has no name".into(),

View file

@ -2,11 +2,14 @@
use std::ops::{Deref, DerefMut};
use std::time::{Duration, Instant};
use matrix_sdk::ruma::events::space::child::SpaceChildEventContent;
use matrix_sdk::ruma::events::StateEventType;
use matrix_sdk::{
room::Room as MatrixRoom,
ruma::{OwnedRoomId, RoomId},
};
use modalkit::prelude::{EditInfo, InfoMessage};
use ratatui::{
buffer::Buffer,
layout::Rect,
@ -22,9 +25,18 @@ use modalkit_ratatui::{
WindowOps,
};
use crate::base::{IambBufferId, IambInfo, ProgramStore, RoomFocus};
use crate::base::{
IambBufferId,
IambError,
IambInfo,
IambResult,
ProgramContext,
ProgramStore,
RoomFocus,
SpaceAction,
};
use crate::windows::{room_fields_cmp, RoomItem};
use crate::windows::{room_fields_cmp, RoomItem, RoomLikeItem};
const SPACE_HIERARCHY_DEBOUNCE: Duration = Duration::from_secs(5);
@ -68,6 +80,71 @@ impl SpaceState {
last_fetch: self.last_fetch,
}
}
pub async fn space_command(
&mut self,
act: SpaceAction,
_: ProgramContext,
store: &mut ProgramStore,
) -> IambResult<EditInfo> {
match act {
SpaceAction::SetChild(child_id, order, suggested) => {
if !self
.room
.can_user_send_state(
&store.application.settings.profile.user_id,
StateEventType::SpaceChild,
)
.await
.map_err(IambError::from)?
{
return Err(IambError::InsufficientPermission.into());
}
let via = self.room.route().await.map_err(IambError::from)?;
let mut ev = SpaceChildEventContent::new(via);
ev.order = order;
ev.suggested = suggested;
let _ = self
.room
.send_state_event_for_key(&child_id, ev)
.await
.map_err(IambError::from)?;
Ok(InfoMessage::from("Space updated").into())
},
SpaceAction::RemoveChild => {
let space = self.list.get().ok_or(IambError::NoSelectedRoomOrSpaceItem)?;
if !self
.room
.can_user_send_state(
&store.application.settings.profile.user_id,
StateEventType::SpaceChild,
)
.await
.map_err(IambError::from)?
{
return Err(IambError::InsufficientPermission.into());
}
let ev = SpaceChildEventContent::new(vec![]);
let event_id = self
.room
.send_state_event_for_key(&space.room_id().to_owned(), ev)
.await
.map_err(IambError::from)?;
// Fix for element (see https://github.com/element-hq/element-web/issues/29606)
let _ = self
.room
.redact(&event_id.event_id, Some("workaround for element bug"), None)
.await
.map_err(IambError::from)?;
Ok(InfoMessage::from("Room removed").into())
},
}
}
}
impl TerminalCursor for SpaceState {