From f31119a013ef47864aeb83bfa4099e64c87e5a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 11 Mar 2025 16:01:56 +0100 Subject: [PATCH] refactor(multiverse): Turn the read receipt rendering logic into a widget --- labs/multiverse/src/main.rs | 88 ++++++++++------------------ labs/multiverse/src/read_receipts.rs | 65 ++++++++++++++++++++ labs/multiverse/src/room_list.rs | 36 +++++++++++- 3 files changed, 131 insertions(+), 58 deletions(-) create mode 100644 labs/multiverse/src/read_receipts.rs diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index ce162760a..7d362dc57 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -11,13 +11,13 @@ use color_eyre::Result; use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use futures_util::{pin_mut, StreamExt as _}; use imbl::Vector; +use layout::Flex; use matrix_sdk::{ authentication::matrix::MatrixSession, config::StoreConfig, encryption::{BackupDownloadStrategy, EncryptionSettings}, reqwest::Url, ruma::{ - api::client::receipt::create_receipt::v3::ReceiptType, events::room::message::{MessageType, RoomMessageEventContent}, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, }, @@ -34,11 +34,13 @@ use matrix_sdk_ui::{ Timeline as SdkTimeline, }; use ratatui::{prelude::*, style::palette::tailwind, widgets::*}; +use read_receipts::ReadReceipts; use status::Status; use tokio::{runtime::Handle, spawn, task::JoinHandle}; use tracing::{error, warn}; use tracing_subscriber::EnvFilter; +mod read_receipts; mod room_list; mod status; @@ -67,6 +69,16 @@ struct Cli { proxy: Option, } +/// Helper function to create a centered rect using up certain percentage of the +/// available rect `r` +fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect { + let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center); + let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center); + let [area] = vertical.areas(area); + let [area] = horizontal.areas(area); + area +} + #[tokio::main] async fn main() -> Result<()> { let file_writer = tracing_appender::rolling::hourly("/tmp/", "logs-"); @@ -159,8 +171,14 @@ impl App { // stopped. sync_service.start().await; - let room_list = RoomList::new(rooms, ui_rooms.clone(), room_infos, sync_service.clone()); let status = Status::new(); + let room_list = RoomList::new( + rooms, + ui_rooms.clone(), + room_infos, + sync_service.clone(), + status.handle(), + ); Ok(Self { sync_service, @@ -277,31 +295,6 @@ impl App { } } - /// Mark the currently selected room as read. - async fn mark_as_read(&mut self) { - let Some(room) = self - .room_list - .get_selected_room_id() - .and_then(|room_id| self.ui_rooms.lock().get(&room_id).cloned()) - else { - self.status.set_message("missing room or nothing to show".to_owned()); - return; - }; - - // Mark as read! - match room.timeline().unwrap().mark_as_read(ReceiptType::Read).await { - Ok(did) => { - self.status.set_message(format!( - "did {}send a read receipt!", - if did { "" } else { "not " } - )); - } - Err(err) => { - self.status.set_message(format!("error when marking a room as read: {err}",)); - } - } - } - async fn toggle_reaction_to_latest_msg(&mut self) { let selected = self.room_list.get_selected_room_id(); @@ -433,7 +426,11 @@ impl App { Char('L') => self.toggle_reaction_to_latest_msg().await, - Char('r') => self.set_mode(DetailsMode::ReadReceipts), + Char('r') => { + if self.room_list.get_selected_room_id().is_some() { + self.set_mode(DetailsMode::ReadReceipts); + } + } Char('t') => self.set_mode(DetailsMode::TimelineItems), Char('e') => self.set_mode(DetailsMode::Events), Char('l') => self.set_mode(DetailsMode::LinkedChunk), @@ -446,7 +443,7 @@ impl App { } Char('m') if self.details_mode == DetailsMode::ReadReceipts => { - self.mark_as_read().await + self.room_list.mark_as_read().await } _ => {} @@ -542,35 +539,14 @@ impl App { if let Some(room_id) = self.room_list.get_selected_room_id() { match self.details_mode { DetailsMode::ReadReceipts => { - // In read receipts mode, show the read receipts object as computed - // by the client. - match self.ui_rooms.lock().get(&room_id).cloned() { - Some(room) => { - let receipts = room.read_receipts(); - render_paragraph( - buf, - format!( - r#"Read receipts: -- unread: {} -- notifications: {} -- mentions: {} + // In read receipts mode, show the read receipts object as computed by the + // client. + let rooms = self.ui_rooms.lock(); + let room = rooms.get(&room_id); ---- + let mut read_receipts = ReadReceipts::new(room); -{:?} -"#, - receipts.num_unread, - receipts.num_notifications, - receipts.num_mentions, - receipts - ), - ) - } - None => render_paragraph( - buf, - "(room disappeared in the room list service)".to_owned(), - ), - } + read_receipts.render(inner_area, buf); } DetailsMode::TimelineItems => { diff --git a/labs/multiverse/src/read_receipts.rs b/labs/multiverse/src/read_receipts.rs new file mode 100644 index 000000000..bc31eb298 --- /dev/null +++ b/labs/multiverse/src/read_receipts.rs @@ -0,0 +1,65 @@ +use matrix_sdk_ui::room_list_service::Room; +use ratatui::{ + prelude::*, + widgets::{Block, Clear, Paragraph, Wrap}, +}; + +use crate::{popup_area, TEXT_COLOR}; + +pub struct ReadReceipts<'a> { + room: Option<&'a Room>, +} + +impl<'a> ReadReceipts<'a> { + pub fn new(room: Option<&'a Room>) -> Self { + Self { room } + } +} + +impl Widget for &mut ReadReceipts<'_> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let read_receipt_block = Block::bordered().title("Read receipts"); + let read_receipt_area = popup_area(area, 80, 80); + Clear.render(read_receipt_area, buf); + + match self.room { + Some(room) => { + let receipts = room.read_receipts(); + let content = format!( + r#"Read receipts: +- unread: {} +- notifications: {} +- mentions: {} + +--- + +{:?} +"#, + receipts.num_unread, + receipts.num_notifications, + receipts.num_mentions, + receipts + ); + + Paragraph::new(content) + .block(read_receipt_block.clone()) + .fg(TEXT_COLOR) + .wrap(Wrap { trim: false }) + .render(read_receipt_area, buf); + } + None => { + let content = "(room disappeared in the room list service)"; + Paragraph::new(content) + .block(read_receipt_block.clone()) + .fg(TEXT_COLOR) + .wrap(Wrap { trim: false }) + .render(read_receipt_area, buf); + } + } + + read_receipt_block.render(read_receipt_area, buf); + } +} diff --git a/labs/multiverse/src/room_list.rs b/labs/multiverse/src/room_list.rs index 3c5467c1d..b77ee006e 100644 --- a/labs/multiverse/src/room_list.rs +++ b/labs/multiverse/src/room_list.rs @@ -4,11 +4,14 @@ use std::{ }; use imbl::Vector; -use matrix_sdk::ruma::OwnedRoomId; +use matrix_sdk::ruma::{api::client::receipt::create_receipt::v3::ReceiptType, OwnedRoomId}; use matrix_sdk_ui::{room_list_service, sync_service::SyncService}; use ratatui::{prelude::*, widgets::*}; -use crate::{UiRooms, ALT_ROW_COLOR, HEADER_BG, NORMAL_ROW_COLOR, SELECTED_STYLE_FG, TEXT_COLOR}; +use crate::{ + status::StatusHandle, UiRooms, ALT_ROW_COLOR, HEADER_BG, NORMAL_ROW_COLOR, SELECTED_STYLE_FG, + TEXT_COLOR, +}; /// Extra room information, like its display name, etc. #[derive(Clone)] @@ -29,6 +32,8 @@ pub type RoomInfos = Arc>>; pub struct RoomList { pub state: ListState, + pub status_handle: StatusHandle, + pub rooms: Rooms, /// Room list service rooms known to the app. @@ -50,10 +55,12 @@ impl RoomList { ui_rooms: UiRooms, room_infos: RoomInfos, sync_service: Arc, + status_handle: StatusHandle, ) -> Self { Self { state: Default::default(), rooms, + status_handle, room_infos, current_room_subscription: None, ui_rooms, @@ -130,6 +137,31 @@ impl RoomList { self.current_room_subscription = Some(room); } } + + /// Mark the currently selected room as read. + pub async fn mark_as_read(&mut self) { + let Some(room) = self + .get_selected_room_id() + .and_then(|room_id| self.ui_rooms.lock().get(&room_id).cloned()) + else { + self.status_handle.set_message("missing room or nothing to show".to_owned()); + return; + }; + + // Mark as read! + match room.timeline().unwrap().mark_as_read(ReceiptType::Read).await { + Ok(did) => { + self.status_handle.set_message(format!( + "did {}send a read receipt!", + if did { "" } else { "not " } + )); + } + Err(err) => { + self.status_handle + .set_message(format!("error when marking a room as read: {err}",)); + } + } + } } impl Widget for &mut RoomList {