feat(multiverse): Add support to join rooms you've been invited to

This commit is contained in:
Damir Jelić
2025-04-11 10:05:33 +02:00
parent d36b2a6869
commit bc50cae35f
6 changed files with 556 additions and 166 deletions

81
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View 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);
}
}
}
}

View File

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