From 2aa1c95c5b547396d7a9095498ea39330a8cd866 Mon Sep 17 00:00:00 2001 From: jrichardsen Date: Thu, 9 Jan 2025 14:09:47 +0100 Subject: [PATCH 1/6] improve title bar in direct messages In direct messages, show the room title (i.e., the username) in the color of the user's status. Also, show the user's status text in parentheses. Fixes #35 Signed-off-by: jrichardsen --- src/backend/nc_request/nc_req_data_user.rs | 4 +- src/ui/widget/title_bar.rs | 71 +++++++++++++++++++--- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/backend/nc_request/nc_req_data_user.rs b/src/backend/nc_request/nc_req_data_user.rs index ec791e8..9ba4cf1 100644 --- a/src/backend/nc_request/nc_req_data_user.rs +++ b/src/backend/nc_request/nc_req_data_user.rs @@ -13,8 +13,8 @@ pub struct NCReqDataParticipants { attendeePermissions: i32, sessionIds: Vec, pub status: Option, - statusIcon: Option, - statusMessage: Option, + pub statusIcon: Option, + pub statusMessage: Option, statusClearAt: Option, roomToken: Option, phoneNumber: Option, diff --git a/src/ui/widget/title_bar.rs b/src/ui/widget/title_bar.rs index 185e5d7..3d8032e 100644 --- a/src/ui/widget/title_bar.rs +++ b/src/ui/widget/title_bar.rs @@ -12,6 +12,12 @@ use style::Styled; pub struct TitleBar<'a> { room: String, + status: Option, + status_text: Option, + user_away_style: Style, + user_dnd_style: Style, + user_online_style: Style, + user_offline_style: Style, mode: String, unread: usize, unread_rooms: Text<'a>, @@ -24,6 +30,12 @@ impl TitleBar<'_> { pub fn new(initial_state: CurrentScreen, room: String, config: &Config) -> Self { TitleBar { room, + status: None, + status_text: None, + user_away_style: config.theme.user_away_style(), + user_dnd_style: config.theme.user_dnd_style(), + user_online_style: config.theme.user_online_style(), + user_offline_style: config.theme.user_offline_style(), mode: initial_state.to_string(), unread: 0, unread_rooms: Text::raw(""), @@ -40,7 +52,25 @@ impl TitleBar<'_> { current_room: &Token, ) { self.mode = screen.to_string(); - self.room = backend.get_room(current_room).to_string(); + let room = backend.get_room(current_room); + self.room = room.to_string(); + if room.is_dm() { + let user = room + .get_users() + .into_iter() + .find(|user| &user.displayName == room.get_display_name()); + if user.is_none() { + log::warn!("Could not find user associated with this DM"); + } + self.status = user.and_then(|user| (&user.status).clone()); + self.status_text = + user.and_then(|user| match (&user.statusIcon, &user.statusMessage) { + (None, None) => None, + (None, Some(msg)) => Some(String::from(msg)), + (Some(icon), None) => Some(String::from(icon)), + (Some(icon), Some(msg)) => Some(String::from(format!("{icon} {msg}"))), + }); + } self.unread = backend.get_room(current_room).get_unread(); let unread_array: Vec = backend .get_unread_rooms() @@ -65,19 +95,41 @@ impl TitleBar<'_> { impl Widget for &TitleBar<'_> { fn render(self, area: Rect, buf: &mut Buffer) { - let (room_title, room_title_style) = if self.unread > 0 { - ( - format!("Current: {}: {}", self.room, self.unread), - self.title_style, - ) + let header = if self.unread > 0 { + format!("Current({}): ", self.unread) + } else { + String::from("Current: ") + }; + let room_style = if let Some(status) = &self.status { + match status.as_str() { + "away" => self.user_away_style, + "offline" => self.user_offline_style, + "dnd" => self.user_dnd_style, + "online" => self.user_online_style, + unknown => { + log::debug!("Unknown Status {unknown}"); + self.default_style + } + } } else { - (format!("Current: {}", self.room), self.title_style) + self.title_style }; + let mut title_length = header.len() + self.room.len(); + let mut title_spans = vec![ + Span::styled(header, self.title_style), + Span::styled(&self.room, room_style), + ]; + + if let Some(status_text) = &self.status_text { + let status_text = format!(" ({status_text})"); + title_length += status_text.len(); + title_spans.push(Span::styled(status_text, self.title_style)); + } let title_layout = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Min(room_title.len().as_()), + Constraint::Min(title_length.as_()), Constraint::Fill(1), Constraint::Percentage(20), ]) @@ -87,7 +139,7 @@ impl Widget for &TitleBar<'_> { .borders(Borders::BOTTOM) .style(self.default_style); - Paragraph::new(Text::styled(room_title, room_title_style)) + Paragraph::new(Line::from(title_spans)) .block(title_block) .render(title_layout[0], buf); @@ -138,6 +190,7 @@ mod tests { dummy_user.displayName = "Butz".to_string(); mock_room.expect_get_users().return_const(vec![dummy_user]); mock_room.expect_get_unread().return_const(false); + mock_room.expect_is_dm().return_const(false); mock_nc_backend .expect_get_unread_rooms() .once() From 3614b72705f854001fd58a5a5778e1a41447bcff Mon Sep 17 00:00:00 2001 From: jrichardsen Date: Thu, 9 Jan 2025 14:20:06 +0100 Subject: [PATCH 2/6] clippy Signed-off-by: jrichardsen --- src/ui/widget/title_bar.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/widget/title_bar.rs b/src/ui/widget/title_bar.rs index 3d8032e..06165b6 100644 --- a/src/ui/widget/title_bar.rs +++ b/src/ui/widget/title_bar.rs @@ -57,18 +57,18 @@ impl TitleBar<'_> { if room.is_dm() { let user = room .get_users() - .into_iter() - .find(|user| &user.displayName == room.get_display_name()); + .iter() + .find(|user| user.displayName == room.get_display_name()); if user.is_none() { log::warn!("Could not find user associated with this DM"); } - self.status = user.and_then(|user| (&user.status).clone()); + self.status = user.and_then(|user| user.status.clone()); self.status_text = user.and_then(|user| match (&user.statusIcon, &user.statusMessage) { (None, None) => None, (None, Some(msg)) => Some(String::from(msg)), (Some(icon), None) => Some(String::from(icon)), - (Some(icon), Some(msg)) => Some(String::from(format!("{icon} {msg}"))), + (Some(icon), Some(msg)) => Some(format!("{icon} {msg}")), }); } self.unread = backend.get_room(current_room).get_unread(); From 881de590d60db49a0e4f50986c95aab516eee7eb Mon Sep 17 00:00:00 2001 From: jrichardsen Date: Thu, 9 Jan 2025 14:43:44 +0100 Subject: [PATCH 3/6] add test Signed-off-by: jrichardsen --- src/ui/widget/title_bar.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/ui/widget/title_bar.rs b/src/ui/widget/title_bar.rs index 06165b6..539cc31 100644 --- a/src/ui/widget/title_bar.rs +++ b/src/ui/widget/title_bar.rs @@ -181,16 +181,19 @@ mod tests { let config = init("./test/").unwrap(); let mut mock_nc_backend = MockNCTalk::new(); - let backend = TestBackend::new(30, 3); + let backend = TestBackend::new(60, 3); let mut terminal = Terminal::new(backend).unwrap(); let mut bar = TitleBar::new(CurrentScreen::Reading, "General".to_string(), &config); let mut mock_room = MockNCRoomInterface::new(); let mut dummy_user = NCReqDataParticipants::default(); dummy_user.displayName = "Butz".to_string(); + dummy_user.status = Some(String::from("online")); + dummy_user.statusMessage = Some(String::from("having fun")); mock_room.expect_get_users().return_const(vec![dummy_user]); - mock_room.expect_get_unread().return_const(false); - mock_room.expect_is_dm().return_const(false); + mock_room.expect_get_unread().return_const(42_usize); + mock_room.expect_is_dm().return_const(true); + mock_room.expect_get_display_name().return_const(String::from("Butz")); mock_nc_backend .expect_get_unread_rooms() .once() @@ -202,19 +205,21 @@ mod tests { bar.update(CurrentScreen::Reading, &mock_nc_backend, &"123".to_string()); terminal - .draw(|frame| bar.render_area(frame, Rect::new(0, 0, 30, 3))) + .draw(|frame| bar.render_area(frame, Rect::new(0, 0, 60, 3))) .unwrap(); let mut expected = Buffer::with_lines([ - "Current: Butz Readin", - " ", - "──────────────────────────────", + "Current(42): Butz (having fun) Reading", + " ", + "────────────────────────────────────────────────────────────", ]); - expected.set_style(Rect::new(0, 0, 30, 3), config.theme.default_style()); + expected.set_style(Rect::new(0, 0, 60, 3), config.theme.default_style()); expected.set_style(Rect::new(0, 0, 13, 1), config.theme.title_status_style()); + expected.set_style(Rect::new(13, 0, 4, 1), config.theme.user_online_style()); + expected.set_style(Rect::new(17, 0, 13, 1), config.theme.title_status_style()); - expected.set_style(Rect::new(24, 0, 6, 1), config.theme.title_status_style()); + expected.set_style(Rect::new(53, 0, 7, 1), config.theme.title_status_style()); terminal.backend().assert_buffer(&expected); } From 08ac838659ee2bef01b186193e64a72e286675b0 Mon Sep 17 00:00:00 2001 From: jrichardsen Date: Thu, 9 Jan 2025 14:45:47 +0100 Subject: [PATCH 4/6] use to_string() instead of String::from() Signed-off-by: jrichardsen --- src/ui/widget/title_bar.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/widget/title_bar.rs b/src/ui/widget/title_bar.rs index 539cc31..d41a93d 100644 --- a/src/ui/widget/title_bar.rs +++ b/src/ui/widget/title_bar.rs @@ -66,8 +66,8 @@ impl TitleBar<'_> { self.status_text = user.and_then(|user| match (&user.statusIcon, &user.statusMessage) { (None, None) => None, - (None, Some(msg)) => Some(String::from(msg)), - (Some(icon), None) => Some(String::from(icon)), + (None, Some(msg)) => Some(msg.to_string()), + (Some(icon), None) => Some(icon.to_string()), (Some(icon), Some(msg)) => Some(format!("{icon} {msg}")), }); } @@ -98,7 +98,7 @@ impl Widget for &TitleBar<'_> { let header = if self.unread > 0 { format!("Current({}): ", self.unread) } else { - String::from("Current: ") + "Current: ".to_string() }; let room_style = if let Some(status) = &self.status { match status.as_str() { @@ -188,12 +188,12 @@ mod tests { let mut mock_room = MockNCRoomInterface::new(); let mut dummy_user = NCReqDataParticipants::default(); dummy_user.displayName = "Butz".to_string(); - dummy_user.status = Some(String::from("online")); - dummy_user.statusMessage = Some(String::from("having fun")); + dummy_user.status = Some("online".to_string()); + dummy_user.statusMessage = Some("having fun".to_string()); mock_room.expect_get_users().return_const(vec![dummy_user]); mock_room.expect_get_unread().return_const(42_usize); mock_room.expect_is_dm().return_const(true); - mock_room.expect_get_display_name().return_const(String::from("Butz")); + mock_room.expect_get_display_name().return_const("Butz".to_string()); mock_nc_backend .expect_get_unread_rooms() .once() From d24f443916c37d063a6e0fa52113b0365a8b3687 Mon Sep 17 00:00:00 2001 From: jrichardsen Date: Thu, 9 Jan 2025 14:48:57 +0100 Subject: [PATCH 5/6] rustfmt Signed-off-by: jrichardsen --- src/ui/widget/title_bar.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/widget/title_bar.rs b/src/ui/widget/title_bar.rs index d41a93d..4c15759 100644 --- a/src/ui/widget/title_bar.rs +++ b/src/ui/widget/title_bar.rs @@ -193,7 +193,9 @@ mod tests { mock_room.expect_get_users().return_const(vec![dummy_user]); mock_room.expect_get_unread().return_const(42_usize); mock_room.expect_is_dm().return_const(true); - mock_room.expect_get_display_name().return_const("Butz".to_string()); + mock_room + .expect_get_display_name() + .return_const("Butz".to_string()); mock_nc_backend .expect_get_unread_rooms() .once() From 0441c40bffd631fe32fac36c212701fb368e884d Mon Sep 17 00:00:00 2001 From: jrichardsen Date: Fri, 10 Jan 2025 10:27:35 +0100 Subject: [PATCH 6/6] Generate title widget in update method Signed-off-by: jrichardsen --- src/ui/app.rs | 2 +- src/ui/widget/title_bar.rs | 68 ++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 9bf565c..7a16e80 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -77,7 +77,7 @@ impl App<'_, Backend> { Self { current_screen: CurrentScreen::Reading, - title: TitleBar::new(CurrentScreen::Reading, init_room.clone(), config), + title: TitleBar::new(CurrentScreen::Reading, config), selector: ChatSelector::new(&backend, config), input: InputBox::new("", config), chat: { diff --git a/src/ui/widget/title_bar.rs b/src/ui/widget/title_bar.rs index 4c15759..262b950 100644 --- a/src/ui/widget/title_bar.rs +++ b/src/ui/widget/title_bar.rs @@ -11,9 +11,7 @@ use ratatui::{ use style::Styled; pub struct TitleBar<'a> { - room: String, - status: Option, - status_text: Option, + title: Line<'a>, user_away_style: Style, user_dnd_style: Style, user_online_style: Style, @@ -27,11 +25,9 @@ pub struct TitleBar<'a> { } impl TitleBar<'_> { - pub fn new(initial_state: CurrentScreen, room: String, config: &Config) -> Self { + pub fn new(initial_state: CurrentScreen, config: &Config) -> Self { TitleBar { - room, - status: None, - status_text: None, + title: Line::from(vec![]), user_away_style: config.theme.user_away_style(), user_dnd_style: config.theme.user_dnd_style(), user_online_style: config.theme.user_online_style(), @@ -53,25 +49,26 @@ impl TitleBar<'_> { ) { self.mode = screen.to_string(); let room = backend.get_room(current_room); - self.room = room.to_string(); + let room_name = room.get_display_name(); + let mut status = None; + let mut status_text = None; if room.is_dm() { let user = room .get_users() .iter() .find(|user| user.displayName == room.get_display_name()); if user.is_none() { - log::warn!("Could not find user associated with this DM"); + log::error!("Could not find user associated with this DM"); } - self.status = user.and_then(|user| user.status.clone()); - self.status_text = - user.and_then(|user| match (&user.statusIcon, &user.statusMessage) { - (None, None) => None, - (None, Some(msg)) => Some(msg.to_string()), - (Some(icon), None) => Some(icon.to_string()), - (Some(icon), Some(msg)) => Some(format!("{icon} {msg}")), - }); + status = user.and_then(|user| user.status.clone()); + status_text = user.and_then(|user| match (&user.statusIcon, &user.statusMessage) { + (None, None) => None, + (None, Some(msg)) => Some(msg.to_string()), + (Some(icon), None) => Some(icon.to_string()), + (Some(icon), Some(msg)) => Some(format!("{icon} {msg}")), + }); } - self.unread = backend.get_room(current_room).get_unread(); + self.unread = room.get_unread(); let unread_array: Vec = backend .get_unread_rooms() .iter() @@ -86,21 +83,12 @@ impl TitleBar<'_> { Text::raw("UNREAD: ".to_owned() + unread_array.join(", ").as_str()) .set_style(self.title_important_style) }; - } - - pub fn render_area(&self, frame: &mut Frame, area: Rect) { - frame.render_widget(self, area); - } -} - -impl Widget for &TitleBar<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { let header = if self.unread > 0 { format!("Current({}): ", self.unread) } else { "Current: ".to_string() }; - let room_style = if let Some(status) = &self.status { + let room_style = if let Some(status) = &status { match status.as_str() { "away" => self.user_away_style, "offline" => self.user_offline_style, @@ -114,22 +102,29 @@ impl Widget for &TitleBar<'_> { } else { self.title_style }; - let mut title_length = header.len() + self.room.len(); let mut title_spans = vec![ Span::styled(header, self.title_style), - Span::styled(&self.room, room_style), + Span::styled(room_name.to_owned(), room_style), ]; - if let Some(status_text) = &self.status_text { + if let Some(status_text) = &status_text { let status_text = format!(" ({status_text})"); - title_length += status_text.len(); title_spans.push(Span::styled(status_text, self.title_style)); } + self.title = Line::from(title_spans); + } + pub fn render_area(&self, frame: &mut Frame, area: Rect) { + frame.render_widget(self, area); + } +} + +impl Widget for &TitleBar<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { let title_layout = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Min(title_length.as_()), + Constraint::Min(self.title.to_string().len().as_()), Constraint::Fill(1), Constraint::Percentage(20), ]) @@ -139,7 +134,7 @@ impl Widget for &TitleBar<'_> { .borders(Borders::BOTTOM) .style(self.default_style); - Paragraph::new(Line::from(title_spans)) + Paragraph::new(self.title.clone()) .block(title_block) .render(title_layout[0], buf); @@ -183,7 +178,6 @@ mod tests { let mut mock_nc_backend = MockNCTalk::new(); let backend = TestBackend::new(60, 3); let mut terminal = Terminal::new(backend).unwrap(); - let mut bar = TitleBar::new(CurrentScreen::Reading, "General".to_string(), &config); let mut mock_room = MockNCRoomInterface::new(); let mut dummy_user = NCReqDataParticipants::default(); @@ -202,8 +196,10 @@ mod tests { .return_const(vec![]); mock_nc_backend .expect_get_room() - .times(2) + .once() .return_const(mock_room); + + let mut bar = TitleBar::new(CurrentScreen::Reading, &config); bar.update(CurrentScreen::Reading, &mock_nc_backend, &"123".to_string()); terminal