mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-04 05:58:11 -04:00
feat(multiverse): Add support to join rooms you've been invited to
This commit is contained in:
81
Cargo.lock
generated
81
Cargo.lock
generated
@@ -521,9 +521,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1031,7 +1031,7 @@ version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"crossterm_winapi",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
@@ -1258,6 +1258,37 @@ dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "difflib"
|
||||
version = "0.4.0"
|
||||
@@ -1403,12 +1434,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2671,7 +2702,7 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -2934,7 +2965,7 @@ dependencies = [
|
||||
"assert_matches2",
|
||||
"assign",
|
||||
"async-trait",
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"decancer",
|
||||
"eyeball",
|
||||
"eyeball-im",
|
||||
@@ -3410,6 +3441,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tui-framework-experiment",
|
||||
"tui-textarea",
|
||||
]
|
||||
|
||||
@@ -3573,7 +3605,7 @@ version = "0.10.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -3988,7 +4020,7 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"rand",
|
||||
@@ -4027,7 +4059,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"memchr",
|
||||
"pulldown-cmark-escape",
|
||||
"unicase",
|
||||
@@ -4172,7 +4204,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
@@ -4228,7 +4260,7 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4574,7 +4606,7 @@ version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
@@ -4615,7 +4647,7 @@ version = "0.38.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -4731,7 +4763,7 @@ version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -5563,6 +5595,21 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tui-framework-experiment"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743912880bcd21d1034063a1b5c6630d444d5a6cc9f90e2c0a200bbe278907c7"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"crossterm",
|
||||
"derive_builder",
|
||||
"itertools 0.14.0",
|
||||
"ratatui",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui-textarea"
|
||||
version = "0.7.0"
|
||||
@@ -6377,7 +6424,7 @@ version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -2964,6 +2964,7 @@ impl Room {
|
||||
/// this room.
|
||||
pub async fn invite_details(&self) -> Result<Invite> {
|
||||
let state = self.state();
|
||||
|
||||
if state != RoomState::Invited {
|
||||
return Err(Error::WrongRoomState(Box::new(WrongRoomState::new("Invited", state))));
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui" }
|
||||
ratatui = { version = "0.29.0", features = ["unstable-widget-ref"] }
|
||||
throbber-widgets-tui = "0.8.0"
|
||||
tui-textarea = "0.7.0"
|
||||
tui-framework-experiment = "0.4.0"
|
||||
rpassword = "7.3.1"
|
||||
serde_json = { workspace = true }
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
|
||||
@@ -8,7 +8,12 @@ use std::{
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use crossterm::{
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
|
||||
},
|
||||
execute,
|
||||
};
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
use imbl::Vector;
|
||||
use layout::Flex;
|
||||
@@ -110,6 +115,7 @@ async fn main() -> Result<()> {
|
||||
event_cache.enable_storage()?;
|
||||
|
||||
let terminal = ratatui::init();
|
||||
execute!(stdout(), EnableMouseCapture)?;
|
||||
let mut app = App::new(client).await?;
|
||||
|
||||
app.run(terminal).await
|
||||
@@ -315,29 +321,38 @@ impl App {
|
||||
self.state.global_mode = mode;
|
||||
}
|
||||
|
||||
async fn handle_global_key_press(&mut self, key: KeyEvent) -> Result<bool> {
|
||||
async fn handle_global_event(&mut self, event: Event) -> Result<bool> {
|
||||
use KeyCode::*;
|
||||
|
||||
match (key.modifiers, key.code) {
|
||||
(KeyModifiers::NONE, F(1)) => self.set_global_mode(GlobalMode::Help),
|
||||
match event {
|
||||
Event::Key(KeyEvent { code: F(1), modifiers: KeyModifiers::NONE, .. }) => {
|
||||
self.set_global_mode(GlobalMode::Help)
|
||||
}
|
||||
|
||||
(KeyModifiers::NONE, F(10)) => self.set_global_mode(GlobalMode::Settings {
|
||||
view: SettingsView::new(self.client.clone(), self.sync_service.clone()),
|
||||
}),
|
||||
Event::Key(KeyEvent { code: F(10), modifiers: KeyModifiers::NONE, .. }) => self
|
||||
.set_global_mode(GlobalMode::Settings {
|
||||
view: SettingsView::new(self.client.clone(), self.sync_service.clone()),
|
||||
}),
|
||||
|
||||
(KeyModifiers::CONTROL, Char('j') | Down) => {
|
||||
Event::Key(KeyEvent {
|
||||
code: Char('j') | Down,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => {
|
||||
self.room_list.next_room();
|
||||
let room_id = self.room_list.get_selected_room_id();
|
||||
self.room_view.set_selected_room(room_id);
|
||||
}
|
||||
|
||||
(KeyModifiers::CONTROL, Char('k') | Up) => {
|
||||
Event::Key(KeyEvent {
|
||||
code: Char('k') | Up, modifiers: KeyModifiers::CONTROL, ..
|
||||
}) => {
|
||||
self.room_list.previous_room();
|
||||
let room_id = self.room_list.get_selected_room_id();
|
||||
self.room_view.set_selected_room(room_id);
|
||||
}
|
||||
|
||||
(KeyModifiers::CONTROL, Char('q')) => {
|
||||
Event::Key(KeyEvent { code: Char('q'), modifiers: KeyModifiers::CONTROL, .. }) => {
|
||||
if !matches!(self.state.global_mode, GlobalMode::Default) {
|
||||
self.set_global_mode(GlobalMode::Default);
|
||||
} else {
|
||||
@@ -345,7 +360,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
_ => self.room_view.handle_key_press(key).await,
|
||||
_ => self.room_view.handle_event(event).await,
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
@@ -369,43 +384,44 @@ impl App {
|
||||
terminal.draw(|f| f.render_widget(&mut *self, f.area()))?;
|
||||
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match &mut self.state.global_mode {
|
||||
GlobalMode::Default => {
|
||||
if self.handle_global_key_press(key).await? {
|
||||
let sync_service = self.sync_service.clone();
|
||||
let timelines = self.timelines.clone();
|
||||
let listen_task = self.listen_task.abort_handle();
|
||||
let event = event::read()?;
|
||||
|
||||
let shutdown_task = spawn(async move {
|
||||
sync_service.stop().await;
|
||||
match &mut self.state.global_mode {
|
||||
GlobalMode::Default => {
|
||||
if self.handle_global_event(event).await? {
|
||||
let sync_service = self.sync_service.clone();
|
||||
let timelines = self.timelines.clone();
|
||||
let listen_task = self.listen_task.abort_handle();
|
||||
|
||||
listen_task.abort();
|
||||
let shutdown_task = spawn(async move {
|
||||
sync_service.stop().await;
|
||||
|
||||
for timeline in timelines.lock().values() {
|
||||
timeline.task.abort();
|
||||
}
|
||||
});
|
||||
listen_task.abort();
|
||||
|
||||
self.set_global_mode(GlobalMode::Exiting { shutdown_task });
|
||||
for timeline in timelines.lock().values() {
|
||||
timeline.task.abort();
|
||||
}
|
||||
}
|
||||
GlobalMode::Help => {
|
||||
if let (KeyModifiers::NONE, Char('q') | Esc) =
|
||||
(key.modifiers, key.code)
|
||||
{
|
||||
self.set_global_mode(GlobalMode::Default)
|
||||
}
|
||||
}
|
||||
GlobalMode::Settings { view } => {
|
||||
if view.handle_key_press(key).await {
|
||||
self.set_global_mode(GlobalMode::Default);
|
||||
}
|
||||
}
|
||||
GlobalMode::Exiting { .. } => {}
|
||||
});
|
||||
|
||||
self.set_global_mode(GlobalMode::Exiting { shutdown_task });
|
||||
}
|
||||
}
|
||||
GlobalMode::Help => {
|
||||
if let Event::Key(key) = event {
|
||||
if let (KeyModifiers::NONE, Char('q') | Esc) = (key.modifiers, key.code)
|
||||
{
|
||||
self.set_global_mode(GlobalMode::Default)
|
||||
}
|
||||
}
|
||||
}
|
||||
GlobalMode::Settings { view } => {
|
||||
if let Event::Key(key) = event {
|
||||
if view.handle_key_press(key).await {
|
||||
self.set_global_mode(GlobalMode::Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
GlobalMode::Exiting { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +448,7 @@ impl App {
|
||||
|
||||
// At this point the user has exited the loop, so shut down the application.
|
||||
ratatui::restore();
|
||||
execute!(stdout(), DisableMouseCapture)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
274
labs/multiverse/src/widgets/room_view/invited_room.rs
Normal file
274
labs/multiverse/src/widgets/room_view/invited_room.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
|
||||
use futures_util::FutureExt;
|
||||
use matrix_sdk::{room::Invite, RoomState};
|
||||
use matrix_sdk_ui::room_list_service::Room;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use throbber_widgets_tui::{Throbber, ThrobberState};
|
||||
use tokio::{spawn, task::JoinHandle};
|
||||
use tui_framework_experiment::{button, Button};
|
||||
|
||||
use crate::widgets::recovery::create_centered_throbber_area;
|
||||
|
||||
enum Mode {
|
||||
Loading { task: JoinHandle<Result<Invite, matrix_sdk::Error>> },
|
||||
Joining { task: JoinHandle<Result<(), matrix_sdk::Error>> },
|
||||
Leaving { task: JoinHandle<Result<(), matrix_sdk::Error>> },
|
||||
Loaded { invite_details: Invite },
|
||||
Done,
|
||||
}
|
||||
|
||||
impl Drop for Mode {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
Mode::Loading { task } => task.abort(),
|
||||
Mode::Joining { task } => task.abort(),
|
||||
Mode::Leaving { task } => task.abort(),
|
||||
Mode::Loaded { .. } => {}
|
||||
Mode::Done => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FocusedButton {
|
||||
Accept = 0,
|
||||
Reject = 1,
|
||||
}
|
||||
|
||||
impl TryFrom<usize> for FocusedButton {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(FocusedButton::Accept),
|
||||
1 => Ok(FocusedButton::Reject),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InvitedRoomView {
|
||||
mode: Mode,
|
||||
room: Room,
|
||||
buttons: Buttons,
|
||||
}
|
||||
|
||||
struct Buttons {
|
||||
areas: Vec<Rect>,
|
||||
focused_button: FocusedButton,
|
||||
accept: Button<'static>,
|
||||
reject: Button<'static>,
|
||||
}
|
||||
|
||||
impl Buttons {
|
||||
fn focused_button_mut(&mut self) -> &mut Button<'static> {
|
||||
match self.focused_button {
|
||||
FocusedButton::Accept => &mut self.accept,
|
||||
FocusedButton::Reject => &mut self.reject,
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_next_button(&mut self) {
|
||||
self.focused_button_mut().normal();
|
||||
|
||||
match self.focused_button {
|
||||
FocusedButton::Accept => self.focused_button = FocusedButton::Reject,
|
||||
FocusedButton::Reject => self.focused_button = FocusedButton::Accept,
|
||||
}
|
||||
|
||||
self.focused_button_mut().select();
|
||||
}
|
||||
|
||||
fn release(&mut self) {
|
||||
self.focused_button_mut().select();
|
||||
}
|
||||
|
||||
fn click(&mut self, column: u16, row: u16) -> bool {
|
||||
for (i, area) in self.areas.iter().enumerate() {
|
||||
let area_contains_click = area.left() <= column
|
||||
&& column < area.right()
|
||||
&& area.top() <= row
|
||||
&& row < area.bottom();
|
||||
|
||||
if area_contains_click {
|
||||
match i.try_into() {
|
||||
Ok(FocusedButton::Accept) => {
|
||||
self.release();
|
||||
self.accept.toggle_press();
|
||||
self.focused_button = FocusedButton::Accept;
|
||||
return true;
|
||||
}
|
||||
Ok(FocusedButton::Reject) => {
|
||||
self.release();
|
||||
self.reject.toggle_press();
|
||||
self.focused_button = FocusedButton::Accept;
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl InvitedRoomView {
|
||||
pub(super) fn new(room: Room) -> Self {
|
||||
let task = spawn({
|
||||
let room = room.clone();
|
||||
async move { room.invite_details().await }
|
||||
});
|
||||
|
||||
let mut accept = Button::new("Accept").with_theme(button::themes::GREEN);
|
||||
accept.select();
|
||||
|
||||
let mode = Mode::Loading { task };
|
||||
let buttons = Buttons {
|
||||
focused_button: FocusedButton::Accept,
|
||||
accept,
|
||||
reject: Button::new("Reject").with_theme(button::themes::RED),
|
||||
areas: Vec::new(),
|
||||
};
|
||||
|
||||
Self { mode, room, buttons }
|
||||
}
|
||||
|
||||
fn join_or_leave(&mut self) {
|
||||
let room = self.room.clone();
|
||||
|
||||
let mode = match self.buttons.focused_button {
|
||||
FocusedButton::Accept => {
|
||||
Mode::Joining { task: spawn(async move { room.join().await }) }
|
||||
}
|
||||
FocusedButton::Reject => {
|
||||
Mode::Leaving { task: spawn(async move { room.leave().await }) }
|
||||
}
|
||||
};
|
||||
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, event: Event) {
|
||||
use KeyCode::*;
|
||||
|
||||
match event {
|
||||
Event::Key(KeyEvent { code: Char('j') | Left, .. }) => self.buttons.focus_next_button(),
|
||||
Event::Key(KeyEvent { code: Char('k') | Right, .. }) => {
|
||||
self.buttons.focus_next_button()
|
||||
}
|
||||
Event::Key(KeyEvent { code: Char(' ') | Enter, .. }) => {
|
||||
self.buttons.focused_button_mut().toggle_press();
|
||||
self.join_or_leave()
|
||||
}
|
||||
|
||||
Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column,
|
||||
row,
|
||||
..
|
||||
}) => {
|
||||
if self.buttons.click(column, row) {
|
||||
self.join_or_leave()
|
||||
}
|
||||
}
|
||||
|
||||
Event::Mouse(MouseEvent { kind: MouseEventKind::Up(MouseButton::Left), .. }) => {
|
||||
self.buttons.release();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
if !matches!(self.room.state(), RoomState::Invited) {
|
||||
self.mode = Mode::Done;
|
||||
} else {
|
||||
match &mut self.mode {
|
||||
Mode::Loading { task } => {
|
||||
if task.is_finished() {
|
||||
let invite_details = task
|
||||
.now_or_never()
|
||||
.expect("We checked that the task has finished")
|
||||
.expect("The task shouldn't ever panic")
|
||||
.expect("We should be able to load the invite details from storage");
|
||||
self.mode = Mode::Loaded { invite_details };
|
||||
}
|
||||
}
|
||||
Mode::Joining { .. } => {}
|
||||
Mode::Leaving { .. } => {}
|
||||
Mode::Loaded { .. } => {}
|
||||
Mode::Done => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_switch(&self) -> bool {
|
||||
matches!(self.mode, Mode::Done)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut InvitedRoomView {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.update();
|
||||
|
||||
let mut create_throbber = |title| {
|
||||
let centered = create_centered_throbber_area(area);
|
||||
let mut state = ThrobberState::default();
|
||||
state.calc_step(0);
|
||||
|
||||
let throbber = Throbber::default()
|
||||
.label(title)
|
||||
.throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
|
||||
|
||||
StatefulWidget::render(throbber, centered, buf, &mut state);
|
||||
};
|
||||
|
||||
match &self.mode {
|
||||
Mode::Loading { .. } => create_throbber("Loading"),
|
||||
Mode::Leaving { .. } => create_throbber("Rejecting"),
|
||||
Mode::Joining { .. } | Mode::Done => create_throbber("Joining"),
|
||||
Mode::Loaded { invite_details } => {
|
||||
let text = if let Some(inviter) = &invite_details.inviter {
|
||||
let display_name =
|
||||
inviter.display_name().unwrap_or_else(|| inviter.user_id().as_str());
|
||||
|
||||
format!("{display_name} has invited you to this room")
|
||||
} else {
|
||||
"You have been invited to this room".to_owned()
|
||||
};
|
||||
|
||||
let [_, middle_area, _] = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints([Constraint::Fill(1), Constraint::Length(5), Constraint::Fill(1)])
|
||||
.areas(area);
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(5)]);
|
||||
let [label_area, button_area] = vertical.areas(middle_area);
|
||||
|
||||
let paragraph = Paragraph::new(text).centered();
|
||||
paragraph.render(label_area, buf);
|
||||
|
||||
let [_, left_button, _, right_button, _] = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(20),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(20),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(button_area);
|
||||
|
||||
self.buttons.areas = vec![left_button, right_button];
|
||||
|
||||
self.buttons.accept.render(left_button, buf);
|
||||
self.buttons.reject.render(right_button, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||
use invited_room::InvitedRoomView;
|
||||
use matrix_sdk::{
|
||||
locks::Mutex,
|
||||
ruma::{
|
||||
api::client::receipt::create_receipt::v3::ReceiptType,
|
||||
events::room::message::RoomMessageEventContent, OwnedRoomId,
|
||||
},
|
||||
RoomState,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use tokio::{spawn, task::JoinHandle};
|
||||
@@ -20,18 +22,14 @@ use crate::{
|
||||
|
||||
mod details;
|
||||
mod input;
|
||||
mod invited_room;
|
||||
mod timeline;
|
||||
|
||||
const DEFAULT_TILING_DIRECTION: Direction = Direction::Horizontal;
|
||||
|
||||
#[derive(Default)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
Details {
|
||||
tiling_direction: Direction,
|
||||
view: RoomDetails,
|
||||
},
|
||||
Normal { invited_room_view: Option<InvitedRoomView> },
|
||||
Details { tiling_direction: Direction, view: RoomDetails },
|
||||
}
|
||||
|
||||
pub struct RoomView {
|
||||
@@ -60,109 +58,142 @@ impl RoomView {
|
||||
timelines,
|
||||
status_handle,
|
||||
current_pagination: Default::default(),
|
||||
mode: Mode::default(),
|
||||
mode: Mode::Normal { invited_room_view: None },
|
||||
input: Input::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_key_press(&mut self, key: KeyEvent) {
|
||||
pub async fn handle_event(&mut self, event: Event) {
|
||||
use KeyCode::*;
|
||||
|
||||
match &mut self.mode {
|
||||
Mode::Normal => match (key.modifiers, key.code) {
|
||||
(KeyModifiers::NONE, Enter) => {
|
||||
if !self.input.is_empty() {
|
||||
let message = self.input.get_text();
|
||||
Mode::Normal { invited_room_view } => {
|
||||
if let Some(view) = invited_room_view {
|
||||
view.handle_event(event);
|
||||
} else if let Event::Key(key) = event {
|
||||
match (key.modifiers, key.code) {
|
||||
(KeyModifiers::NONE, Enter) => {
|
||||
if !self.input.is_empty() {
|
||||
let message = self.input.get_text();
|
||||
|
||||
match self.send_message(message).await {
|
||||
Ok(_) => {
|
||||
self.input.clear();
|
||||
}
|
||||
Err(err) => {
|
||||
self.status_handle
|
||||
.set_message(format!("error when sending event: {err}"));
|
||||
match self.send_message(message).await {
|
||||
Ok(_) => {
|
||||
self.input.clear();
|
||||
}
|
||||
Err(err) => {
|
||||
self.status_handle.set_message(format!(
|
||||
"error when sending event: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(KeyModifiers::CONTROL, Char('l')) => self.toggle_reaction_to_latest_msg().await,
|
||||
|
||||
(KeyModifiers::NONE, PageUp) => self.back_paginate(),
|
||||
|
||||
(KeyModifiers::ALT, Char('e')) => {
|
||||
if self.selected_room.is_some() {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: DEFAULT_TILING_DIRECTION,
|
||||
view: RoomDetails::with_events_as_selected(),
|
||||
(KeyModifiers::CONTROL, Char('l')) => {
|
||||
self.toggle_reaction_to_latest_msg().await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(KeyModifiers::ALT, Char('r')) => {
|
||||
if self.selected_room.is_some() {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: DEFAULT_TILING_DIRECTION,
|
||||
view: RoomDetails::with_receipts_as_selected(),
|
||||
(KeyModifiers::NONE, PageUp) => self.back_paginate(),
|
||||
|
||||
(KeyModifiers::ALT, Char('e')) => {
|
||||
if self.selected_room.is_some() {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: DEFAULT_TILING_DIRECTION,
|
||||
view: RoomDetails::with_events_as_selected(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(KeyModifiers::ALT, Char('l')) => {
|
||||
if self.selected_room.is_some() {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: DEFAULT_TILING_DIRECTION,
|
||||
view: RoomDetails::with_chunks_as_selected(),
|
||||
(KeyModifiers::ALT, Char('r')) => {
|
||||
if self.selected_room.is_some() {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: DEFAULT_TILING_DIRECTION,
|
||||
view: RoomDetails::with_receipts_as_selected(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(KeyModifiers::ALT, Char('l')) => {
|
||||
if self.selected_room.is_some() {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: DEFAULT_TILING_DIRECTION,
|
||||
view: RoomDetails::with_chunks_as_selected(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => self.input.handle_key_press(key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => self.input.handle_key_press(key),
|
||||
},
|
||||
Mode::Details { view, tiling_direction } => {
|
||||
if let Event::Key(key) = event {
|
||||
match (key.modifiers, key.code) {
|
||||
(KeyModifiers::NONE, PageUp) => self.back_paginate(),
|
||||
|
||||
Mode::Details { view, tiling_direction } => match (key.modifiers, key.code) {
|
||||
(KeyModifiers::NONE, PageUp) => self.back_paginate(),
|
||||
(KeyModifiers::ALT, Char('t')) => {
|
||||
let new_layout = match tiling_direction {
|
||||
Direction::Horizontal => Direction::Vertical,
|
||||
Direction::Vertical => Direction::Horizontal,
|
||||
};
|
||||
|
||||
(KeyModifiers::ALT, Char('t')) => {
|
||||
let new_layout = match tiling_direction {
|
||||
Direction::Horizontal => Direction::Vertical,
|
||||
Direction::Vertical => Direction::Horizontal,
|
||||
};
|
||||
*tiling_direction = new_layout;
|
||||
}
|
||||
|
||||
*tiling_direction = new_layout;
|
||||
}
|
||||
(KeyModifiers::ALT, Char('e')) => {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: *tiling_direction,
|
||||
view: RoomDetails::with_events_as_selected(),
|
||||
}
|
||||
}
|
||||
|
||||
(KeyModifiers::ALT, Char('e')) => {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: *tiling_direction,
|
||||
view: RoomDetails::with_events_as_selected(),
|
||||
(KeyModifiers::ALT, Char('r')) => {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: *tiling_direction,
|
||||
view: RoomDetails::with_receipts_as_selected(),
|
||||
}
|
||||
}
|
||||
|
||||
(KeyModifiers::ALT, Char('l')) => {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: *tiling_direction,
|
||||
view: RoomDetails::with_chunks_as_selected(),
|
||||
}
|
||||
}
|
||||
|
||||
_ => match view.handle_key_press(key) {
|
||||
ShouldExit::No => {}
|
||||
ShouldExit::OnlySubScreen => {}
|
||||
ShouldExit::Yes => self.mode = Mode::Normal { invited_room_view: None },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
(KeyModifiers::ALT, Char('r')) => {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: *tiling_direction,
|
||||
view: RoomDetails::with_receipts_as_selected(),
|
||||
}
|
||||
}
|
||||
|
||||
(KeyModifiers::ALT, Char('l')) => {
|
||||
self.mode = Mode::Details {
|
||||
tiling_direction: *tiling_direction,
|
||||
view: RoomDetails::with_chunks_as_selected(),
|
||||
}
|
||||
}
|
||||
|
||||
_ => match view.handle_key_press(key) {
|
||||
ShouldExit::No => {}
|
||||
ShouldExit::OnlySubScreen => {}
|
||||
ShouldExit::Yes => self.mode = Mode::Normal,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_selected_room(&mut self, room: Option<OwnedRoomId>) {
|
||||
if let Some(room_id) = room.as_deref() {
|
||||
let rooms = self.ui_rooms.lock();
|
||||
let maybe_room = rooms.get(room_id);
|
||||
|
||||
if let Some(room) = maybe_room {
|
||||
if matches!(room.state(), RoomState::Invited) {
|
||||
let room = room.clone();
|
||||
let view = InvitedRoomView::new(room);
|
||||
self.mode = Mode::Normal { invited_room_view: Some(view) }
|
||||
} else {
|
||||
match &mut self.mode {
|
||||
Mode::Normal { invited_room_view } => {
|
||||
invited_room_view.take();
|
||||
}
|
||||
Mode::Details { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.selected_room = room;
|
||||
}
|
||||
|
||||
@@ -266,6 +297,17 @@ impl RoomView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
match &mut self.mode {
|
||||
Mode::Normal { invited_room_view } => {
|
||||
if invited_room_view.as_ref().is_some_and(|view| view.should_switch()) {
|
||||
self.mode = Mode::Normal { invited_room_view: None };
|
||||
}
|
||||
}
|
||||
Mode::Details { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut RoomView {
|
||||
@@ -273,6 +315,8 @@ impl Widget for &mut RoomView {
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.update();
|
||||
|
||||
// Create a space for the header, timeline, and input area.
|
||||
let vertical =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)]);
|
||||
@@ -304,37 +348,43 @@ impl Widget for &mut RoomView {
|
||||
|
||||
if let Some(room_id) = self.selected_room.as_deref() {
|
||||
let rooms = self.ui_rooms.lock();
|
||||
let mut room = rooms.get(room_id);
|
||||
let mut maybe_room = rooms.get(room_id);
|
||||
|
||||
let timeline_area = match &mut self.mode {
|
||||
Mode::Normal { invited_room_view } => {
|
||||
if let Some(view) = invited_room_view {
|
||||
view.render(middle_area, buf);
|
||||
|
||||
None
|
||||
} else {
|
||||
self.input.render(input_area, buf, &mut maybe_room);
|
||||
Some(middle_area)
|
||||
}
|
||||
}
|
||||
Mode::Details { tiling_direction, view } => {
|
||||
let vertical = Layout::new(
|
||||
*tiling_direction,
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
);
|
||||
let [timeline_area, details_area] = vertical.areas(middle_area);
|
||||
Clear.render(details_area, buf);
|
||||
|
||||
view.render(details_area, buf, &mut maybe_room);
|
||||
|
||||
Some(timeline_area)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(items) =
|
||||
self.timelines.lock().get(room_id).map(|timeline| timeline.items.clone())
|
||||
{
|
||||
let timeline_area = match &mut self.mode {
|
||||
Mode::Normal => {
|
||||
self.input.render(input_area, buf, &mut room);
|
||||
middle_area
|
||||
}
|
||||
Mode::Details { view, tiling_direction } => {
|
||||
let vertical = Layout::new(
|
||||
*tiling_direction,
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
);
|
||||
let [timeline_area, details_area] = vertical.areas(middle_area);
|
||||
Clear.render(details_area, buf);
|
||||
if let Some(timeline_area) = timeline_area {
|
||||
let items = items.lock();
|
||||
let mut timeline = TimelineView::new(items.deref());
|
||||
|
||||
view.render(details_area, buf, &mut room);
|
||||
|
||||
timeline_area
|
||||
}
|
||||
};
|
||||
|
||||
let items = items.lock();
|
||||
let mut timeline = TimelineView::new(items.deref());
|
||||
|
||||
timeline.render(timeline_area, buf);
|
||||
} else {
|
||||
render_paragraph(buf, "(room's timeline disappeared)".to_owned())
|
||||
};
|
||||
timeline.render(timeline_area, buf);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
render_paragraph(buf, "Nothing to see here...".to_owned())
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user