diff --git a/Cargo.lock b/Cargo.lock index c21dae6eb..7df401544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index ebb213d3f..f97f4952d 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2964,6 +2964,7 @@ impl Room { /// this room. pub async fn invite_details(&self) -> Result { let state = self.state(); + if state != RoomState::Invited { return Err(Error::WrongRoomState(Box::new(WrongRoomState::new("Invited", state)))); } diff --git a/labs/multiverse/Cargo.toml b/labs/multiverse/Cargo.toml index f60a405dc..5b89cb871 100644 --- a/labs/multiverse/Cargo.toml +++ b/labs/multiverse/Cargo.toml @@ -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"] } diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index d388242bb..492b7bc18 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -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 { + async fn handle_global_event(&mut self, event: Event) -> Result { 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(()) } diff --git a/labs/multiverse/src/widgets/room_view/invited_room.rs b/labs/multiverse/src/widgets/room_view/invited_room.rs new file mode 100644 index 000000000..79c91bdc5 --- /dev/null +++ b/labs/multiverse/src/widgets/room_view/invited_room.rs @@ -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> }, + Joining { task: JoinHandle> }, + Leaving { task: JoinHandle> }, + 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 for FocusedButton { + type Error = (); + + fn try_from(value: usize) -> Result { + match value { + 0 => Ok(FocusedButton::Accept), + 1 => Ok(FocusedButton::Reject), + _ => Err(()), + } + } +} + +pub struct InvitedRoomView { + mode: Mode, + room: Room, + buttons: Buttons, +} + +struct Buttons { + areas: Vec, + 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); + } + } + } +} diff --git a/labs/multiverse/src/widgets/room_view/mod.rs b/labs/multiverse/src/widgets/room_view/mod.rs index 42f496771..631f458c7 100644 --- a/labs/multiverse/src/widgets/room_view/mod.rs +++ b/labs/multiverse/src/widgets/room_view/mod.rs @@ -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 }, + 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) { + 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()) };