Support listing room members (#6)

This commit is contained in:
Ulyssa 2023-01-04 12:51:33 -08:00
parent d038da6844
commit 8ed037afca
No known key found for this signature in database
GPG key ID: 1B3965A3D18B9B64
11 changed files with 316 additions and 52 deletions

4
Cargo.lock generated
View file

@ -1600,9 +1600,9 @@ dependencies = [
[[package]] [[package]]
name = "modalkit" name = "modalkit"
version = "0.0.7" version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc4385b7136847cd063fbf093f0aa21a098398e26dc9213137fdd4e4f593d6bf" checksum = "7f4e400066e546471efee517b7e5e3ca5af2c04014e76289aecc7af621011bba"
dependencies = [ dependencies = [
"anymap2", "anymap2",
"bitflags", "bitflags",

View file

@ -19,7 +19,7 @@ dirs = "4.0.0"
futures = "0.3.21" futures = "0.3.21"
gethostname = "0.4.1" gethostname = "0.4.1"
matrix-sdk = {version = "0.6", default-features = false, features = ["e2e-encryption", "sled", "rustls-tls"]} matrix-sdk = {version = "0.6", default-features = false, features = ["e2e-encryption", "sled", "rustls-tls"]}
modalkit = "0.0.7" modalkit = "0.0.8"
regex = "^1.5" regex = "^1.5"
rpassword = "^7.2" rpassword = "^7.2"
serde = "^1.0" serde = "^1.0"

View file

@ -42,7 +42,7 @@ two other TUI clients and Element Web:
| Room tag showing | :x: ([#15]) | :heavy_check_mark: | :x: | :heavy_check_mark: | | Room tag showing | :x: ([#15]) | :heavy_check_mark: | :x: | :heavy_check_mark: |
| Room tag editing | :x: ([#15]) | :heavy_check_mark: | :x: | :heavy_check_mark: | | Room tag editing | :x: ([#15]) | :heavy_check_mark: | :x: | :heavy_check_mark: |
| Search joined rooms | :x: ([#16]) | :heavy_check_mark: | :x: | :heavy_check_mark: | | Search joined rooms | :x: ([#16]) | :heavy_check_mark: | :x: | :heavy_check_mark: |
| Room user list | :x: ([#6]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Room user list | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Display Room Description | :x: ([#12]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Display Room Description | :x: ([#12]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Edit Room Description | :x: ([#12]) | :x: | :heavy_check_mark: | :heavy_check_mark: | | Edit Room Description | :x: ([#12]) | :x: | :heavy_check_mark: | :heavy_check_mark: |
| Highlights | :x: ([#8]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Highlights | :x: ([#8]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |

View file

@ -26,7 +26,7 @@ use modalkit::{
store::Store, store::Store,
}, },
env::vim::{ env::vim::{
command::{VimCommand, VimCommandMachine}, command::{CommandContext, VimCommand, VimCommandMachine},
keybindings::VimMachine, keybindings::VimMachine,
VimContext, VimContext,
}, },
@ -59,8 +59,14 @@ pub enum VerifyAction {
Mismatch, Mismatch,
} }
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RoomAction {
Members(Box<CommandContext<ProgramContext>>),
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum IambAction { pub enum IambAction {
Room(RoomAction),
Verify(VerifyAction, String), Verify(VerifyAction, String),
VerifyRequest(String), VerifyRequest(String),
SendMessage(OwnedRoomId, String), SendMessage(OwnedRoomId, String),
@ -70,6 +76,7 @@ pub enum IambAction {
impl ApplicationAction for IambAction { impl ApplicationAction for IambAction {
fn is_edit_sequence<C: EditContext>(&self, _: &C) -> SequenceStatus { fn is_edit_sequence<C: EditContext>(&self, _: &C) -> SequenceStatus {
match self { match self {
IambAction::Room(..) => SequenceStatus::Break,
IambAction::SendMessage(..) => SequenceStatus::Break, IambAction::SendMessage(..) => SequenceStatus::Break,
IambAction::ToggleScrollbackFocus => SequenceStatus::Break, IambAction::ToggleScrollbackFocus => SequenceStatus::Break,
IambAction::Verify(..) => SequenceStatus::Break, IambAction::Verify(..) => SequenceStatus::Break,
@ -79,6 +86,7 @@ impl ApplicationAction for IambAction {
fn is_last_action<C: EditContext>(&self, _: &C) -> SequenceStatus { fn is_last_action<C: EditContext>(&self, _: &C) -> SequenceStatus {
match self { match self {
IambAction::Room(..) => SequenceStatus::Atom,
IambAction::SendMessage(..) => SequenceStatus::Atom, IambAction::SendMessage(..) => SequenceStatus::Atom,
IambAction::ToggleScrollbackFocus => SequenceStatus::Atom, IambAction::ToggleScrollbackFocus => SequenceStatus::Atom,
IambAction::Verify(..) => SequenceStatus::Atom, IambAction::Verify(..) => SequenceStatus::Atom,
@ -88,6 +96,7 @@ impl ApplicationAction for IambAction {
fn is_last_selection<C: EditContext>(&self, _: &C) -> SequenceStatus { fn is_last_selection<C: EditContext>(&self, _: &C) -> SequenceStatus {
match self { match self {
IambAction::Room(..) => SequenceStatus::Ignore,
IambAction::SendMessage(..) => SequenceStatus::Ignore, IambAction::SendMessage(..) => SequenceStatus::Ignore,
IambAction::ToggleScrollbackFocus => SequenceStatus::Ignore, IambAction::ToggleScrollbackFocus => SequenceStatus::Ignore,
IambAction::Verify(..) => SequenceStatus::Ignore, IambAction::Verify(..) => SequenceStatus::Ignore,
@ -97,6 +106,7 @@ impl ApplicationAction for IambAction {
fn is_switchable<C: EditContext>(&self, _: &C) -> bool { fn is_switchable<C: EditContext>(&self, _: &C) -> bool {
match self { match self {
IambAction::Room(..) => false,
IambAction::SendMessage(..) => false, IambAction::SendMessage(..) => false,
IambAction::ToggleScrollbackFocus => false, IambAction::ToggleScrollbackFocus => false,
IambAction::Verify(..) => false, IambAction::Verify(..) => false,
@ -275,6 +285,14 @@ impl ChatStore {
} }
} }
pub fn get_room_title(&self, room_id: &RoomId) -> String {
self.rooms
.get(room_id)
.and_then(|i| i.name.as_ref())
.map(String::from)
.unwrap_or_else(|| "Untitled Matrix Room".to_string())
}
pub fn mark_for_load(&mut self, room_id: OwnedRoomId) { pub fn mark_for_load(&mut self, room_id: OwnedRoomId) {
self.need_load.insert(room_id); self.need_load.insert(room_id);
} }
@ -346,6 +364,7 @@ impl ApplicationStore for ChatStore {}
pub enum IambId { pub enum IambId {
Room(OwnedRoomId), Room(OwnedRoomId),
DirectList, DirectList,
MemberList(OwnedRoomId),
RoomList, RoomList,
SpaceList, SpaceList,
VerifyList, VerifyList,
@ -375,6 +394,7 @@ pub enum IambBufferId {
Command, Command,
Room(OwnedRoomId, RoomFocus), Room(OwnedRoomId, RoomFocus),
DirectList, DirectList,
MemberList(OwnedRoomId),
RoomList, RoomList,
SpaceList, SpaceList,
VerifyList, VerifyList,
@ -387,6 +407,7 @@ impl IambBufferId {
IambBufferId::Command => None, IambBufferId::Command => None,
IambBufferId::Room(room, _) => Some(IambId::Room(room.clone())), IambBufferId::Room(room, _) => Some(IambId::Room(room.clone())),
IambBufferId::DirectList => Some(IambId::DirectList), IambBufferId::DirectList => Some(IambId::DirectList),
IambBufferId::MemberList(room) => Some(IambId::MemberList(room.clone())),
IambBufferId::RoomList => Some(IambId::RoomList), IambBufferId::RoomList => Some(IambId::RoomList),
IambBufferId::SpaceList => Some(IambId::SpaceList), IambBufferId::SpaceList => Some(IambId::SpaceList),
IambBufferId::VerifyList => Some(IambId::VerifyList), IambBufferId::VerifyList => Some(IambId::VerifyList),

View file

@ -1,5 +1,5 @@
use modalkit::{ use modalkit::{
editing::{action::WindowAction, base::OpenTarget}, editing::base::OpenTarget,
env::vim::command::{CommandContext, CommandDescription}, env::vim::command::{CommandContext, CommandDescription},
input::commands::{CommandError, CommandResult, CommandStep}, input::commands::{CommandError, CommandResult, CommandStep},
input::InputContext, input::InputContext,
@ -11,6 +11,7 @@ use crate::base::{
ProgramCommand, ProgramCommand,
ProgramCommands, ProgramCommands,
ProgramContext, ProgramContext,
RoomAction,
VerifyAction, VerifyAction,
}; };
@ -22,8 +23,8 @@ fn iamb_verify(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
match args.len() { match args.len() {
0 => { 0 => {
let open = WindowAction::Switch(OpenTarget::Application(IambId::VerifyList)); let open = ctx.switch(OpenTarget::Application(IambId::VerifyList));
let step = CommandStep::Continue(open.into(), ctx.context.take()); let step = CommandStep::Continue(open, ctx.context.take());
return Ok(step); return Ok(step);
}, },
@ -61,7 +62,18 @@ fn iamb_dms(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
} }
let open = WindowAction::Switch(OpenTarget::Application(IambId::DirectList)); let open = ctx.switch(OpenTarget::Application(IambId::DirectList));
let step = CommandStep::Continue(open, ctx.context.take());
return Ok(step);
}
fn iamb_members(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
if !desc.arg.text.is_empty() {
return Result::Err(CommandError::InvalidArgument);
}
let open = IambAction::Room(RoomAction::Members(ctx.clone().into()));
let step = CommandStep::Continue(open.into(), ctx.context.take()); let step = CommandStep::Continue(open.into(), ctx.context.take());
return Ok(step); return Ok(step);
@ -72,8 +84,8 @@ fn iamb_rooms(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
} }
let open = WindowAction::Switch(OpenTarget::Application(IambId::RoomList)); let open = ctx.switch(OpenTarget::Application(IambId::RoomList));
let step = CommandStep::Continue(open.into(), ctx.context.take()); let step = CommandStep::Continue(open, ctx.context.take());
return Ok(step); return Ok(step);
} }
@ -83,8 +95,8 @@ fn iamb_spaces(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
} }
let open = WindowAction::Switch(OpenTarget::Application(IambId::SpaceList)); let open = ctx.switch(OpenTarget::Application(IambId::SpaceList));
let step = CommandStep::Continue(open.into(), ctx.context.take()); let step = CommandStep::Continue(open, ctx.context.take());
return Ok(step); return Ok(step);
} }
@ -94,8 +106,8 @@ fn iamb_welcome(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
} }
let open = WindowAction::Switch(OpenTarget::Application(IambId::Welcome)); let open = ctx.switch(OpenTarget::Application(IambId::Welcome));
let step = CommandStep::Continue(open.into(), ctx.context.take()); let step = CommandStep::Continue(open, ctx.context.take());
return Ok(step); return Ok(step);
} }
@ -107,8 +119,8 @@ fn iamb_join(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
} }
let open = WindowAction::Switch(args.remove(0)); let open = ctx.switch(args.remove(0));
let step = CommandStep::Continue(open.into(), ctx.context.take()); let step = CommandStep::Continue(open, ctx.context.take());
return Ok(step); return Ok(step);
} }
@ -116,6 +128,7 @@ fn iamb_join(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
fn add_iamb_commands(cmds: &mut ProgramCommands) { fn add_iamb_commands(cmds: &mut ProgramCommands) {
cmds.add_command(ProgramCommand { names: vec!["dms".into()], f: iamb_dms }); cmds.add_command(ProgramCommand { names: vec!["dms".into()], f: iamb_dms });
cmds.add_command(ProgramCommand { names: vec!["join".into()], f: iamb_join }); 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 }); cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms });
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!["verify".into()], f: iamb_verify }); cmds.add_command(ProgramCommand { names: vec!["verify".into()], f: iamb_verify });
@ -133,6 +146,8 @@ pub fn setup_commands() -> ProgramCommands {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use modalkit::editing::action::WindowAction;
#[test] #[test]
fn test_cmd_verify() { fn test_cmd_verify() {
let mut cmds = setup_commands(); let mut cmds = setup_commands();

View file

@ -317,7 +317,7 @@ impl Application {
fn iamb_run( fn iamb_run(
&mut self, &mut self,
action: IambAction, action: IambAction,
_: ProgramContext, ctx: ProgramContext,
store: &mut ProgramStore, store: &mut ProgramStore,
) -> IambResult<EditInfo> { ) -> IambResult<EditInfo> {
let info = match action { let info = match action {
@ -327,6 +327,13 @@ impl Application {
None None
}, },
IambAction::Room(act) => {
let acts = self.screen.current_window_mut()?.room_command(act, ctx, store)?;
self.action_prepend(acts);
None
},
IambAction::SendMessage(room_id, msg) => { IambAction::SendMessage(room_id, msg) => {
let (event_id, msg) = self.worker.send_message(room_id.clone(), msg)?; let (event_id, msg) = self.worker.send_message(room_id.clone(), msg)?;
let user = store.application.settings.profile.user_id.clone(); let user = store.application.settings.profile.user_id.clone();

View file

@ -3,22 +3,23 @@ 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, room::{Room as MatrixRoom, RoomMember},
ruma::RoomId, ruma::{events::room::member::MembershipState, OwnedRoomId, RoomId},
DisplayName, DisplayName,
}; };
use modalkit::tui::{ use modalkit::tui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::{Alignment, Rect},
style::{Modifier as StyleModifier, Style}, style::{Modifier as StyleModifier, Style},
text::{Span, Spans, Text}, text::{Span, Spans, Text},
widgets::{Block, Borders, Widget}, widgets::{Block, Borders, StatefulWidget, Widget},
}; };
use modalkit::{ use modalkit::{
editing::{ editing::{
action::{ action::{
Action,
EditError, EditError,
EditInfo, EditInfo,
EditResult, EditResult,
@ -42,7 +43,7 @@ use modalkit::{
}, },
}, },
widgets::{ widgets::{
list::{ListCursor, ListItem, ListState}, list::{List, ListCursor, ListItem, ListState},
TermOffset, TermOffset,
TerminalCursor, TerminalCursor,
Window, Window,
@ -50,15 +51,19 @@ use modalkit::{
}, },
}; };
use super::base::{ use crate::{
ChatStore, base::{
IambBufferId, ChatStore,
IambId, IambBufferId,
IambInfo, IambId,
IambResult, IambInfo,
ProgramAction, IambResult,
ProgramContext, ProgramAction,
ProgramStore, ProgramContext,
ProgramStore,
RoomAction,
},
message::user_style,
}; };
use self::{room::RoomState, welcome::WelcomeState}; use self::{room::RoomState, welcome::WelcomeState};
@ -120,6 +125,7 @@ macro_rules! delegate {
match $s { match $s {
IambWindow::Room($id) => $e, IambWindow::Room($id) => $e,
IambWindow::DirectList($id) => $e, IambWindow::DirectList($id) => $e,
IambWindow::MemberList($id, _) => $e,
IambWindow::RoomList($id) => $e, IambWindow::RoomList($id) => $e,
IambWindow::SpaceList($id) => $e, IambWindow::SpaceList($id) => $e,
IambWindow::VerifyList($id) => $e, IambWindow::VerifyList($id) => $e,
@ -130,6 +136,7 @@ macro_rules! delegate {
pub enum IambWindow { pub enum IambWindow {
DirectList(DirectListState), DirectList(DirectListState),
MemberList(MemberListState, OwnedRoomId),
Room(RoomState), Room(RoomState),
VerifyList(VerifyListState), VerifyList(VerifyListState),
RoomList(RoomListState), RoomList(RoomListState),
@ -146,19 +153,41 @@ impl IambWindow {
} }
} }
pub fn room_command(
&mut self,
act: RoomAction,
ctx: ProgramContext,
store: &mut ProgramStore,
) -> IambResult<Vec<(Action<IambInfo>, ProgramContext)>> {
if let IambWindow::Room(w) = self {
w.room_command(act, ctx, store)
} else {
let msg = "No room currently focused!";
let err = UIError::Failure(msg.into());
return Err(err);
}
}
pub fn get_title(&self, store: &mut ProgramStore) -> String { pub fn get_title(&self, store: &mut ProgramStore) -> String {
match self { match self {
IambWindow::Room(w) => w.get_title(store),
IambWindow::DirectList(_) => "Direct Messages".to_string(), IambWindow::DirectList(_) => "Direct Messages".to_string(),
IambWindow::RoomList(_) => "Rooms".to_string(), IambWindow::RoomList(_) => "Rooms".to_string(),
IambWindow::SpaceList(_) => "Spaces".to_string(), IambWindow::SpaceList(_) => "Spaces".to_string(),
IambWindow::VerifyList(_) => "Verifications".to_string(), IambWindow::VerifyList(_) => "Verifications".to_string(),
IambWindow::Welcome(_) => "Welcome to iamb".to_string(), IambWindow::Welcome(_) => "Welcome to iamb".to_string(),
IambWindow::Room(w) => w.get_title(store),
IambWindow::MemberList(_, room_id) => {
let title = store.application.get_room_title(room_id.as_ref());
format!("Room Members: {}", title)
},
} }
} }
} }
pub type DirectListState = ListState<DirectItem, IambInfo>; pub type DirectListState = ListState<DirectItem, IambInfo>;
pub type MemberListState = ListState<MemberItem, IambInfo>;
pub type RoomListState = ListState<RoomItem, IambInfo>; pub type RoomListState = ListState<RoomItem, IambInfo>;
pub type SpaceListState = ListState<SpaceItem, IambInfo>; pub type SpaceListState = ListState<SpaceItem, IambInfo>;
pub type VerifyListState = ListState<VerifyItem, IambInfo>; pub type VerifyListState = ListState<VerifyItem, IambInfo>;
@ -263,13 +292,35 @@ impl WindowOps<IambInfo> for IambWindow {
let dms = store.application.worker.direct_messages(); let dms = store.application.worker.direct_messages();
let items = dms.into_iter().map(|(id, name)| DirectItem::new(id, name, store)); let items = dms.into_iter().map(|(id, name)| DirectItem::new(id, name, store));
state.set(items.collect()); state.set(items.collect());
state.draw(inner, buf, focused, store);
List::new(store)
.empty_message("No direct messages yet!")
.empty_alignment(Alignment::Center)
.focus(focused)
.render(inner, buf, state);
},
IambWindow::MemberList(state, room_id) => {
if let Ok(mems) = store.application.worker.members(room_id.clone()) {
let items = mems.into_iter().map(MemberItem::new);
state.set(items.collect());
}
List::new(store)
.empty_message("No users here yet!")
.empty_alignment(Alignment::Center)
.focus(focused)
.render(inner, buf, state);
}, },
IambWindow::RoomList(state) => { IambWindow::RoomList(state) => {
let joined = store.application.worker.joined_rooms(); let joined = store.application.worker.joined_rooms();
let items = joined.into_iter().map(|(id, name)| RoomItem::new(id, name, store)); let items = joined.into_iter().map(|(id, name)| RoomItem::new(id, name, store));
state.set(items.collect()); state.set(items.collect());
state.draw(inner, buf, focused, store);
List::new(store)
.empty_message("You haven't joined any rooms yet")
.empty_alignment(Alignment::Center)
.focus(focused)
.render(inner, buf, state);
}, },
IambWindow::SpaceList(state) => { IambWindow::SpaceList(state) => {
let spaces = store.application.worker.spaces(); let spaces = store.application.worker.spaces();
@ -277,6 +328,12 @@ impl WindowOps<IambInfo> for IambWindow {
spaces.into_iter().map(|(room, name)| SpaceItem::new(room, name, store)); spaces.into_iter().map(|(room, name)| SpaceItem::new(room, name, store));
state.set(items.collect()); state.set(items.collect());
state.draw(inner, buf, focused, store); state.draw(inner, buf, focused, store);
List::new(store)
.empty_message("You haven't joined any spaces yet")
.empty_alignment(Alignment::Center)
.focus(focused)
.render(inner, buf, state);
}, },
IambWindow::VerifyList(state) => { IambWindow::VerifyList(state) => {
let verifications = &store.application.verifications; let verifications = &store.application.verifications;
@ -286,14 +343,29 @@ impl WindowOps<IambInfo> for IambWindow {
items.sort(); items.sort();
state.set(items); state.set(items);
state.draw(inner, buf, focused, store);
List::new(store)
.empty_message("No in-progress verifications")
.empty_alignment(Alignment::Center)
.focus(focused)
.render(inner, buf, state);
}, },
IambWindow::Welcome(state) => state.draw(inner, buf, focused, store), IambWindow::Welcome(state) => state.draw(inner, buf, focused, store),
} }
} }
fn dup(&self, store: &mut ProgramStore) -> Self { fn dup(&self, store: &mut ProgramStore) -> Self {
delegate!(self, w => w.dup(store).into()) match self {
IambWindow::Room(w) => w.dup(store).into(),
IambWindow::DirectList(w) => w.dup(store).into(),
IambWindow::MemberList(w, room_id) => {
IambWindow::MemberList(w.dup(store), room_id.clone())
},
IambWindow::RoomList(w) => w.dup(store).into(),
IambWindow::SpaceList(w) => w.dup(store).into(),
IambWindow::VerifyList(w) => w.dup(store).into(),
IambWindow::Welcome(w) => w.dup(store).into(),
}
} }
fn close(&mut self, flags: CloseFlags, store: &mut ProgramStore) -> bool { fn close(&mut self, flags: CloseFlags, store: &mut ProgramStore) -> bool {
@ -314,6 +386,7 @@ impl Window<IambInfo> for IambWindow {
match self { match self {
IambWindow::Room(room) => IambId::Room(room.id().to_owned()), IambWindow::Room(room) => IambId::Room(room.id().to_owned()),
IambWindow::DirectList(_) => IambId::DirectList, IambWindow::DirectList(_) => IambId::DirectList,
IambWindow::MemberList(_, room_id) => IambId::MemberList(room_id.clone()),
IambWindow::RoomList(_) => IambId::RoomList, IambWindow::RoomList(_) => IambId::RoomList,
IambWindow::SpaceList(_) => IambId::SpaceList, IambWindow::SpaceList(_) => IambId::SpaceList,
IambWindow::VerifyList(_) => IambId::VerifyList, IambWindow::VerifyList(_) => IambId::VerifyList,
@ -334,6 +407,13 @@ impl Window<IambInfo> for IambWindow {
return Ok(list.into()); return Ok(list.into());
}, },
IambId::MemberList(room_id) => {
let id = IambBufferId::MemberList(room_id.clone());
let list = MemberListState::new(id, vec![]);
let win = IambWindow::MemberList(list, room_id);
return Ok(win);
},
IambId::RoomList => { IambId::RoomList => {
let list = RoomListState::new(IambBufferId::RoomList, vec![]); let list = RoomListState::new(IambBufferId::RoomList, vec![]);
@ -412,6 +492,10 @@ 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 {
selected_text(self.name.as_str(), selected) selected_text(self.name.as_str(), selected)
} }
fn get_word(&self) -> Option<String> {
self.room.room_id().to_string().into()
}
} }
impl Promptable<ProgramContext, ProgramStore, IambInfo> for RoomItem { impl Promptable<ProgramContext, ProgramStore, IambInfo> for RoomItem {
@ -451,6 +535,10 @@ impl ListItem<IambInfo> for DirectItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text { fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
selected_text(self.name.as_str(), selected) selected_text(self.name.as_str(), selected)
} }
fn get_word(&self) -> Option<String> {
self.room.room_id().to_string().into()
}
} }
impl Promptable<ProgramContext, ProgramStore, IambInfo> for DirectItem { impl Promptable<ProgramContext, ProgramStore, IambInfo> for DirectItem {
@ -490,6 +578,10 @@ impl ListItem<IambInfo> for SpaceItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text { fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
selected_text(self.name.as_str(), selected) selected_text(self.name.as_str(), selected)
} }
fn get_word(&self) -> Option<String> {
self.room.room_id().to_string().into()
}
} }
impl Promptable<ProgramContext, ProgramStore, IambInfo> for SpaceItem { impl Promptable<ProgramContext, ProgramStore, IambInfo> for SpaceItem {
@ -667,6 +759,10 @@ impl ListItem<IambInfo> for VerifyItem {
Text { lines } Text { lines }
} }
fn get_word(&self) -> Option<String> {
None
}
} }
impl Promptable<ProgramContext, ProgramStore, IambInfo> for VerifyItem { impl Promptable<ProgramContext, ProgramStore, IambInfo> for VerifyItem {
@ -694,3 +790,77 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for VerifyItem {
} }
} }
} }
#[derive(Clone)]
pub struct MemberItem {
member: RoomMember,
}
impl MemberItem {
fn new(member: RoomMember) -> Self {
Self { member }
}
}
impl ToString for MemberItem {
fn to_string(&self) -> String {
self.member.user_id().to_string()
}
}
impl ListItem<IambInfo> for MemberItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
let mut style = user_style(self.member.user_id().as_str());
if selected {
style = style.add_modifier(StyleModifier::REVERSED);
}
let user = Span::styled(self.to_string(), style);
let state = match self.member.membership() {
MembershipState::Ban => Span::raw(" (banned)").into(),
MembershipState::Invite => Span::raw(" (invited)").into(),
MembershipState::Knock => Span::raw(" (wants to join)").into(),
MembershipState::Leave => Span::raw(" (left)").into(),
MembershipState::Join => None,
_ => None,
};
if let Some(state) = state {
Spans(vec![user, state]).into()
} else {
user.into()
}
}
fn get_word(&self) -> Option<String> {
self.member.user_id().to_string().into()
}
}
impl Promptable<ProgramContext, ProgramStore, IambInfo> for MemberItem {
fn prompt(
&mut self,
act: &PromptAction,
_: &ProgramContext,
_: &mut ProgramStore,
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
match act {
PromptAction::Submit => Ok(vec![]),
PromptAction::Abort(_) => {
let msg = "Cannot abort entry inside a list";
let err = EditError::Failure(msg.into());
Err(err)
},
PromptAction::Recall(_, _) => {
let msg = "Cannot recall history inside a list";
let err = EditError::Failure(msg.into());
Err(err)
},
_ => Err(EditError::Unimplemented("unknown prompt action".to_string())),
}
}
}

View file

@ -94,10 +94,6 @@ impl ChatState {
return; return;
} }
if matches!(act, EditorAction::History(_)) {
return;
}
if !store.application.settings.tunables.typing_notice { if !store.application.settings.tunables.typing_notice {
return; return;
} }

View file

@ -6,6 +6,7 @@ use modalkit::tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget};
use modalkit::{ use modalkit::{
editing::action::{ editing::action::{
Action,
EditInfo, EditInfo,
EditResult, EditResult,
Editable, Editable,
@ -15,11 +16,29 @@ use modalkit::{
Promptable, Promptable,
Scrollable, Scrollable,
}, },
editing::base::{CloseFlags, MoveDir1D, PositionList, ScrollStyle, WordStyle}, editing::base::{
Axis,
CloseFlags,
Count,
MoveDir1D,
OpenTarget,
PositionList,
ScrollStyle,
WordStyle,
},
input::InputContext,
widgets::{TermOffset, TerminalCursor, WindowOps}, widgets::{TermOffset, TerminalCursor, WindowOps},
}; };
use crate::base::{IambInfo, IambResult, ProgramAction, ProgramContext, ProgramStore}; use crate::base::{
IambId,
IambInfo,
IambResult,
ProgramAction,
ProgramContext,
ProgramStore,
RoomAction,
};
use self::chat::ChatState; use self::chat::ChatState;
use self::space::{Space, SpaceState}; use self::space::{Space, SpaceState};
@ -67,14 +86,28 @@ impl RoomState {
} }
} }
pub fn room_command(
&mut self,
act: RoomAction,
_: ProgramContext,
_: &mut ProgramStore,
) -> IambResult<Vec<(Action<IambInfo>, ProgramContext)>> {
match act {
RoomAction::Members(mut cmd) => {
let width = Count::Exact(30);
let act =
cmd.default_axis(Axis::Vertical).default_relation(MoveDir1D::Next).window(
OpenTarget::Application(IambId::MemberList(self.id().to_owned())),
width.into(),
);
Ok(vec![(act, cmd.context.take())])
},
}
}
pub fn get_title(&self, store: &mut ProgramStore) -> String { pub fn get_title(&self, store: &mut ProgramStore) -> String {
store store.application.get_room_title(self.id())
.application
.rooms
.get(self.id())
.and_then(|i| i.name.as_ref())
.map(String::from)
.unwrap_or_else(|| "Untitled Matrix Room".to_string())
} }
pub fn focus_toggle(&mut self) { pub fn focus_toggle(&mut self) {

View file

@ -3,8 +3,8 @@
## Useful Keybindings ## Useful Keybindings
- `<Enter>` will send a typed message - `<Enter>` will send a typed message
- `^V^J` can be used in Insert mode to enter a newline without submitting
- `O`/`o` can be used to insert blank lines before and after the cursor line - `O`/`o` can be used to insert blank lines before and after the cursor line
- `^O` can be used in Insert mode to enter a single Normal mode keybinding sequence
- `^Wm` can be used to toggle whether the message bar or scrollback is selected - `^Wm` can be used to toggle whether the message bar or scrollback is selected
- `^Wz` can be used to toggle whether the current window takes up the full screen - `^Wz` can be used to toggle whether the current window takes up the full screen
@ -12,6 +12,7 @@
- `:dms` will open a list of direct messages - `:dms` will open a list of direct messages
- `:rooms` will open a list of joined rooms - `:rooms` will open a list of joined rooms
- `:members` will open a list of members for the currently focused room or space
- `:spaces` will open a list of joined spaces - `:spaces` will open a list of joined spaces
- `:join` can be used to switch to join a new room or start a direct message - `:join` can be used to switch to join a new room or start a direct message
- `:split` and `:vsplit` can be used to open rooms in a new window - `:split` and `:vsplit` can be used to open rooms in a new window

View file

@ -15,7 +15,7 @@ use matrix_sdk::{
encryption::verification::{SasVerification, Verification}, encryption::verification::{SasVerification, Verification},
event_handler::Ctx, event_handler::Ctx,
reqwest, reqwest,
room::{Messages, MessagesOptions, Room as MatrixRoom}, room::{Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
ruma::{ ruma::{
api::client::{ api::client::{
room::create_room::v3::{Request as CreateRoomRequest, RoomPreset}, room::create_room::v3::{Request as CreateRoomRequest, RoomPreset},
@ -102,6 +102,7 @@ pub enum WorkerTask {
GetRoom(OwnedRoomId, ClientReply<IambResult<(MatrixRoom, DisplayName)>>), GetRoom(OwnedRoomId, ClientReply<IambResult<(MatrixRoom, DisplayName)>>),
JoinRoom(String, ClientReply<IambResult<OwnedRoomId>>), JoinRoom(String, ClientReply<IambResult<OwnedRoomId>>),
JoinedRooms(ClientReply<Vec<(MatrixRoom, DisplayName)>>), JoinedRooms(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
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)>>),
SendMessage(OwnedRoomId, String, ClientReply<IambResult<EchoPair>>), SendMessage(OwnedRoomId, String, ClientReply<IambResult<EchoPair>>),
@ -187,6 +188,14 @@ impl Requester {
return response.recv(); return response.recv();
} }
pub fn members(&self, room_id: OwnedRoomId) -> IambResult<Vec<RoomMember>> {
let (reply, response) = oneshot();
self.tx.send(WorkerTask::Members(room_id, reply)).unwrap();
return response.recv();
}
pub fn space_members(&self, space: OwnedRoomId) -> IambResult<Vec<OwnedRoomId>> { pub fn space_members(&self, space: OwnedRoomId) -> IambResult<Vec<OwnedRoomId>> {
let (reply, response) = oneshot(); let (reply, response) = oneshot();
@ -327,6 +336,10 @@ impl ClientWorker {
assert!(self.initialized); assert!(self.initialized);
reply.send(self.login_and_sync(style).await); reply.send(self.login_and_sync(style).await);
}, },
WorkerTask::Members(room_id, reply) => {
assert!(self.initialized);
reply.send(self.members(room_id).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);
@ -744,6 +757,14 @@ impl ClientWorker {
} }
} }
async fn members(&mut self, room_id: OwnedRoomId) -> IambResult<Vec<RoomMember>> {
if let Some(room) = self.client.get_room(room_id.as_ref()) {
Ok(room.active_members().await.map_err(IambError::from)?)
} else {
Err(IambError::UnknownRoom(room_id).into())
}
}
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());