Skip to content

Commit

Permalink
Merge pull request #81 from tofubert/feature/chat-selector-search
Browse files Browse the repository at this point in the history
Add searching functionality to the chat selection screen
  • Loading branch information
jrichardsen authored Jan 10, 2025
2 parents 67d2533 + 8996e4a commit 0484ee4
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 33 deletions.
60 changes: 36 additions & 24 deletions src/ui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ impl<Backend: NCBackend> App<'_, Backend> {
}

pub async fn select_room(&mut self) -> Result<(), Box<dyn std::error::Error>> {
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
Expand All @@ -253,6 +253,7 @@ impl<Backend: NCBackend> App<'_, Backend> {
self.notify.maybe_notify_new_message(
self.backend.select_room(&self.current_room_token).await?,
)?;
self.selector.searching = false;
self.switch_screen(CurrentScreen::Reading);
self.update_ui()?;
self.chat.select_last_message();
Expand Down Expand Up @@ -379,30 +380,41 @@ impl<Backend: NCBackend> App<'_, Backend> {
&mut self,
key: KeyEvent,
) -> Result<(), Box<dyn std::error::Error>> {
match key.code {
KeyCode::Esc => self.switch_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))
});
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('u') | KeyCode::PageUp => {
_ = self.selector.state.select_relative(|current| {
current.map_or(0, |current| current.saturating_sub(9))
});
}
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.switch_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(())
}

Expand Down
105 changes: 96 additions & 9 deletions src/ui/widget/chat_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
items: Vec<TreeItem<'a, String>>,
search_items: Vec<(Token, String)>,
pub search_bar: TextArea<'a>,
pub searching: bool,
default_style: Style,
default_highlight_style: Style,
}
Expand Down Expand Up @@ -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(),
}
Expand Down Expand Up @@ -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::<String>(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(
Expand All @@ -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]);
}
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand Down

0 comments on commit 0484ee4

Please sign in to comment.