From c6345c02349397b24b05fe713ccb658405782e07 Mon Sep 17 00:00:00 2001 From: jrichardsen Date: Fri, 10 Jan 2025 14:59:54 +0100 Subject: [PATCH] Add searching functionality to the chat selection screen Signed-off-by: jrichardsen --- src/ui/app.rs | 60 +++++++++++-------- src/ui/widget/chat_selector.rs | 105 ++++++++++++++++++++++++++++++--- 2 files changed, 132 insertions(+), 33 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 52a9ec3..0006ae9 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -231,7 +231,7 @@ impl App<'_, Backend> { } pub async fn select_room(&mut self) -> Result<(), Box> { - if self.selector.state.selected().len() == 2 { + if self.selector.state.selected().len() == if self.selector.searching { 1 } else { 2 } { self.current_room_token.clone_from( self.selector .state @@ -242,6 +242,7 @@ impl App<'_, Backend> { self.notify.maybe_notify_new_message( self.backend.select_room(&self.current_room_token).await?, )?; + self.selector.searching = false; self.current_screen = CurrentScreen::Reading; self.update_ui()?; self.chat.select_last_message(); @@ -359,30 +360,41 @@ impl App<'_, Backend> { &mut self, key: KeyEvent, ) -> Result<(), Box> { - match key.code { - KeyCode::Esc => self.current_screen = CurrentScreen::Reading, - KeyCode::Char('h') | KeyCode::Left => _ = self.selector.state.key_left(), - KeyCode::Char('j') | KeyCode::Down => _ = self.selector.state.key_down(), - KeyCode::Char('k') | KeyCode::Up => _ = self.selector.state.key_up(), - KeyCode::Char('l') | KeyCode::Right => _ = self.selector.state.key_right(), - KeyCode::Char('d') | KeyCode::PageDown => { - _ = self.selector.state.select_relative(|current| { - current.map_or(0, |current| current.saturating_add(9)) - }); - } - KeyCode::Char('u') | KeyCode::PageUp => { - _ = self.selector.state.select_relative(|current| { - current.map_or(0, |current| current.saturating_sub(9)) - }); + if self.selector.searching { + match key.code { + KeyCode::Down => _ = self.selector.state.key_down(), + KeyCode::Up => _ = self.selector.state.key_up(), + KeyCode::Enter => self.select_room().await?, + KeyCode::Esc => self.selector.searching = false, + _ => _ = self.selector.search_bar.input(key), } - KeyCode::Char('q') => self.popup = Some(Popup::Exit), - KeyCode::Char('?') => self.popup = Some(Popup::Help), - KeyCode::Char(' ') => _ = self.selector.state.toggle_selected(), - KeyCode::Enter => self.select_room().await?, - KeyCode::Home => _ = self.selector.state.select_first(), - KeyCode::End => _ = self.selector.state.select_last(), - _ => (), - }; + } else { + match key.code { + KeyCode::Esc => self.current_screen = CurrentScreen::Reading, + KeyCode::Char('h') | KeyCode::Left => _ = self.selector.state.key_left(), + KeyCode::Char('j') | KeyCode::Down => _ = self.selector.state.key_down(), + KeyCode::Char('k') | KeyCode::Up => _ = self.selector.state.key_up(), + KeyCode::Char('l') | KeyCode::Right => _ = self.selector.state.key_right(), + KeyCode::Char('d') | KeyCode::PageDown => { + _ = self.selector.state.select_relative(|current| { + current.map_or(0, |current| current.saturating_add(9)) + }); + } + KeyCode::Char('u') | KeyCode::PageUp => { + _ = self.selector.state.select_relative(|current| { + current.map_or(0, |current| current.saturating_sub(9)) + }); + } + KeyCode::Char('/') => self.selector.searching = true, + KeyCode::Char('q') => self.popup = Some(Popup::Exit), + KeyCode::Char('?') => self.popup = Some(Popup::Help), + KeyCode::Char(' ') => _ = self.selector.state.toggle_selected(), + KeyCode::Enter => self.select_room().await?, + KeyCode::Home => _ = self.selector.state.select_first(), + KeyCode::End => _ = self.selector.state.select_last(), + _ => (), + }; + } Ok(()) } diff --git a/src/ui/widget/chat_selector.rs b/src/ui/widget/chat_selector.rs index 39cda07..ecd6188 100644 --- a/src/ui/widget/chat_selector.rs +++ b/src/ui/widget/chat_selector.rs @@ -7,15 +7,19 @@ use ratatui::{ Frame, }; +use tui_textarea::TextArea; use tui_tree_widget::{Tree, TreeItem, TreeState}; -use crate::backend::nc_room::NCRoomInterface; use crate::backend::nc_talk::NCBackend; +use crate::backend::{nc_request::Token, nc_room::NCRoomInterface}; use crate::config::Config; pub struct ChatSelector<'a> { pub state: TreeState, items: Vec>, + search_items: Vec<(Token, String)>, + pub search_bar: TextArea<'a>, + pub searching: bool, default_style: Style, default_highlight_style: Style, } @@ -80,6 +84,18 @@ impl ChatSelector<'_> { ) .expect("Group name duplicate"), ], + search_items: backend + .get_room_keys() + .iter() + .map(|&token| { + ( + token.to_string(), + backend.get_room(token).get_display_name().into(), + ) + }) + .collect_vec(), + searching: false, + search_bar: TextArea::new(vec![String::new()]), default_style: config.theme.default_style(), default_highlight_style: config.theme.default_highlight_style(), } @@ -138,11 +154,69 @@ impl ChatSelector<'_> { .collect_vec(), )?, ]; + self.search_items = backend + .get_room_keys() + .iter() + .map(|&token| { + ( + token.to_string(), + backend.get_room(token).get_display_name().into(), + ) + }) + .collect_vec(); Ok(()) } pub fn render_area(&mut self, frame: &mut Frame, area: Rect) { - let widget = Tree::new(&self.items) + let items = if self.searching { + self.search_bar.set_placeholder_text(String::new()); + self.search_bar + .set_block(Block::bordered().border_style(self.default_style)); + self.search_bar.set_style(self.default_highlight_style); + self.search_bar + .set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); + let search_query = self + .search_bar + .lines() + .first() + .expect("Search bar should have at least one line"); + &self + .search_items + .iter() + .filter(|(_, text)| text.to_lowercase().contains(&search_query.to_lowercase())) + .map(|(id, text)| TreeItem::new_leaf::(id.clone(), text.clone())) + .collect_vec() + } else { + self.search_bar + .set_placeholder_text("Type '/' to start searching".to_string()); + self.search_bar.set_placeholder_style(self.default_style); + + // clear the search bar + self.search_bar.cancel_selection(); + self.search_bar.select_all(); + self.search_bar.delete_char(); + + self.search_bar + .set_block(Block::bordered().style(self.default_style)); + self.search_bar.set_cursor_style(Style::default()); + &self.items + }; + + if self.searching { + if let Some(selected) = self.state.selected().first() { + if !items.iter().any(|item| item.identifier() == selected) { + self.state.select(vec![]); + } + } + if self.state.selected().is_empty() { + if let Some(item) = items.first() { + self.state.select(vec![item.identifier().clone()]); + } + } + } + + let layout = Layout::vertical([Constraint::Min(4), Constraint::Length(3)]).split(area); + let widget = Tree::new(items) .expect("all item identifiers are unique") .block(Block::bordered().title("Chat Section")) .experimental_scrollbar(Some( @@ -154,7 +228,8 @@ impl ChatSelector<'_> { .style(self.default_style) .highlight_style(self.default_highlight_style.bold()) .highlight_symbol(">> "); - frame.render_stateful_widget(widget, area, &mut self.state); + frame.render_stateful_widget(widget, layout[0], &mut self.state); + frame.render_widget(&self.search_bar, layout[1]); } } @@ -200,6 +275,12 @@ mod tests { .in_sequence(seq) .return_const(vec![]); + mock_nc_backend + .expect_get_room_keys() + .once() + .in_sequence(seq) + .return_const(vec![]); + mock_nc_backend .expect_get_unread_rooms() .once() @@ -235,6 +316,12 @@ mod tests { .once() .in_sequence(seq) .return_const(vec![(Token::from("Bert"), "2".to_string())]); + + mock_nc_backend + .expect_get_room_keys() + .once() + .in_sequence(seq) + .return_const(vec![]); } #[test] @@ -268,9 +355,9 @@ mod tests { "│ DMs │", "│ Group │", "│ │", - "│ │", - "│ │", - "│ │", + "└──────────────────────────────────────┘", + "┌──────────────────────────────────────┐", + "│ Type '/' to start searching │", "└──────────────────────────────────────┘", ]); expected.set_style(Rect::new(0, 0, 40, 10), config.theme.default_style()); @@ -293,9 +380,9 @@ mod tests { "│ Favorite Chats │", "│ ▶ DMs │", "│ ▶ Group │", - "│ │", - "│ │", - "│ │", + "└──────────────────────────────────────┘", + "┌──────────────────────────────────────┐", + "│ Type '/' to start searching │", "└──────────────────────────────────────┘", ]); expected.set_style(Rect::new(0, 0, 40, 10), config.theme.default_style());