diff --git a/src/app/mod.rs b/src/app/mod.rs index 8d48dc6..d86a479 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -57,6 +57,15 @@ pub struct App { pub pinned_messages: Vec, /// Индекс выбранного pinned сообщения pub selected_pinned_index: usize, + // Message search mode + /// Режим поиска по сообщениям + pub is_message_search_mode: bool, + /// Поисковый запрос + pub message_search_query: String, + /// Результаты поиска + pub message_search_results: Vec, + /// Индекс выбранного результата + pub selected_search_result_index: usize, } impl App { @@ -93,6 +102,10 @@ impl App { is_pinned_mode: false, pinned_messages: Vec::new(), selected_pinned_index: 0, + is_message_search_mode: false, + message_search_query: String::new(), + message_search_results: Vec::new(), + selected_search_result_index: 0, } } @@ -159,6 +172,11 @@ impl App { self.td_client.current_chat_messages.clear(); self.td_client.typing_status = None; self.td_client.current_pinned_message = None; + // Сбрасываем режим поиска + self.is_message_search_mode = false; + self.message_search_query.clear(); + self.message_search_results.clear(); + self.selected_search_result_index = 0; } /// Начать выбор сообщения для редактирования (при стрелке вверх в пустом инпуте) @@ -444,4 +462,59 @@ impl App { pub fn get_selected_pinned_id(&self) -> Option { self.get_selected_pinned().map(|m| m.id) } + + // === Message Search Mode === + + /// Проверить, активен ли режим поиска по сообщениям + pub fn is_message_search_mode(&self) -> bool { + self.is_message_search_mode + } + + /// Войти в режим поиска по сообщениям + pub fn enter_message_search_mode(&mut self) { + self.is_message_search_mode = true; + self.message_search_query.clear(); + self.message_search_results.clear(); + self.selected_search_result_index = 0; + } + + /// Выйти из режима поиска + pub fn exit_message_search_mode(&mut self) { + self.is_message_search_mode = false; + self.message_search_query.clear(); + self.message_search_results.clear(); + self.selected_search_result_index = 0; + } + + /// Установить результаты поиска + pub fn set_search_results(&mut self, results: Vec) { + self.message_search_results = results; + self.selected_search_result_index = 0; + } + + /// Выбрать предыдущий результат (вверх) + pub fn select_previous_search_result(&mut self) { + if self.selected_search_result_index > 0 { + self.selected_search_result_index -= 1; + } + } + + /// Выбрать следующий результат (вниз) + pub fn select_next_search_result(&mut self) { + if !self.message_search_results.is_empty() + && self.selected_search_result_index < self.message_search_results.len() - 1 + { + self.selected_search_result_index += 1; + } + } + + /// Получить текущий выбранный результат + pub fn get_selected_search_result(&self) -> Option<&crate::tdlib::client::MessageInfo> { + self.message_search_results.get(self.selected_search_result_index) + } + + /// Получить ID выбранного результата для перехода + pub fn get_selected_search_result_id(&self) -> Option { + self.get_selected_search_result().map(|m| m.id) + } } diff --git a/src/input/main_input.rs b/src/input/main_input.rs index dd86f25..d2c96c6 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -49,9 +49,75 @@ pub async fn handle(app: &mut App, key: KeyEvent) { } return; } + KeyCode::Char('f') if has_ctrl => { + // Ctrl+F - поиск по сообщениям в открытом чате + if app.selected_chat_id.is_some() && !app.is_pinned_mode() && !app.is_message_search_mode() { + app.enter_message_search_mode(); + } + return; + } _ => {} } + // Режим поиска по сообщениям + if app.is_message_search_mode() { + match key.code { + KeyCode::Esc => { + app.exit_message_search_mode(); + } + KeyCode::Up | KeyCode::Char('N') => { + app.select_previous_search_result(); + } + KeyCode::Down | KeyCode::Char('n') => { + app.select_next_search_result(); + } + KeyCode::Enter => { + // Перейти к выбранному сообщению + if let Some(msg_id) = app.get_selected_search_result_id() { + let msg_index = app.td_client.current_chat_messages + .iter() + .position(|m| m.id == msg_id); + + if let Some(idx) = msg_index { + let total = app.td_client.current_chat_messages.len(); + app.message_scroll_offset = total.saturating_sub(idx + 5); + } + app.exit_message_search_mode(); + } + } + KeyCode::Backspace => { + app.message_search_query.pop(); + // Выполняем поиск при изменении запроса + if let Some(chat_id) = app.get_selected_chat_id() { + if !app.message_search_query.is_empty() { + if let Ok(Ok(results)) = timeout( + Duration::from_secs(3), + app.td_client.search_messages(chat_id, &app.message_search_query) + ).await { + app.set_search_results(results); + } + } else { + app.set_search_results(Vec::new()); + } + } + } + KeyCode::Char(c) => { + app.message_search_query.push(c); + // Выполняем поиск при изменении запроса + if let Some(chat_id) = app.get_selected_chat_id() { + if let Ok(Ok(results)) = timeout( + Duration::from_secs(3), + app.td_client.search_messages(chat_id, &app.message_search_query) + ).await { + app.set_search_results(results); + } + } + } + _ => {} + } + return; + } + // Режим просмотра закреплённых сообщений if app.is_pinned_mode() { match key.code { diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs index 58329f0..9880a1a 100644 --- a/src/tdlib/client.rs +++ b/src/tdlib/client.rs @@ -1165,6 +1165,38 @@ impl TdClient { } } + /// Поиск сообщений в чате по тексту + pub async fn search_messages(&mut self, chat_id: i64, query: &str) -> Result, String> { + if query.trim().is_empty() { + return Ok(Vec::new()); + } + + let result = functions::search_chat_messages( + chat_id, + query.to_string(), + None, // sender_id + 0, // from_message_id + 0, // offset + 50, // limit + None, // filter (no filter = search by text) + 0, // message_thread_id + 0, // saved_messages_topic_id + self.client_id, + ) + .await; + + match result { + Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(found)) => { + let mut messages: Vec = Vec::new(); + for m in found.messages { + messages.push(self.convert_message(&m, chat_id)); + } + Ok(messages) + } + Err(e) => Err(format!("Ошибка поиска: {:?}", e)), + } + } + /// Загрузка старых сообщений (для скролла вверх) pub async fn load_older_messages( &mut self, diff --git a/src/ui/messages.rs b/src/ui/messages.rs index 1bd6646..db7d7c9 100644 --- a/src/ui/messages.rs +++ b/src/ui/messages.rs @@ -307,6 +307,12 @@ fn adjust_entities_for_substring( } pub fn render(f: &mut Frame, area: Rect, app: &App) { + // Режим поиска по сообщениям + if app.is_message_search_mode() { + render_search_mode(f, area, app); + return; + } + // Режим просмотра закреплённых сообщений if app.is_pinned_mode() { render_pinned_mode(f, area, app); @@ -812,6 +818,147 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } } +/// Рендерит режим поиска по сообщениям +fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Search input + Constraint::Min(0), // Search results + Constraint::Length(3), // Help bar + ]) + .split(area); + + // Search input + let total = app.message_search_results.len(); + let current = if total > 0 { app.selected_search_result_index + 1 } else { 0 }; + + let input_line = if app.message_search_query.is_empty() { + Line::from(vec![ + Span::styled("🔍 ", Style::default().fg(Color::Yellow)), + Span::styled("█", Style::default().fg(Color::Yellow)), + Span::styled(" Введите текст для поиска...", Style::default().fg(Color::Gray)), + ]) + } else { + Line::from(vec![ + Span::styled("🔍 ", Style::default().fg(Color::Yellow)), + Span::styled(&app.message_search_query, Style::default().fg(Color::White)), + Span::styled("█", Style::default().fg(Color::Yellow)), + Span::styled(format!(" ({}/{})", current, total), Style::default().fg(Color::Gray)), + ]) + }; + + let search_input = Paragraph::new(input_line) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Yellow)) + .title(" Поиск по сообщениям ") + .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + ); + f.render_widget(search_input, chunks[0]); + + // Search results + let content_width = chunks[1].width.saturating_sub(2) as usize; + let mut lines: Vec = Vec::new(); + + if app.message_search_results.is_empty() { + if !app.message_search_query.is_empty() { + lines.push(Line::from(Span::styled( + "Ничего не найдено", + Style::default().fg(Color::Gray), + ))); + } + } else { + for (idx, msg) in app.message_search_results.iter().enumerate() { + let is_selected = idx == app.selected_search_result_index; + + // Пустая строка между результатами + if idx > 0 { + lines.push(Line::from("")); + } + + // Маркер выбора, имя и дата + let marker = if is_selected { "▶ " } else { " " }; + let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan }; + let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() }; + + lines.push(Line::from(vec![ + Span::styled(marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::styled( + format!("{} ", sender_name), + Style::default().fg(sender_color).add_modifier(Modifier::BOLD), + ), + Span::styled( + format!("({})", crate::utils::format_datetime(msg.date)), + Style::default().fg(Color::Gray), + ), + ])); + + // Текст сообщения (с переносом) + let msg_color = if is_selected { Color::Yellow } else { Color::White }; + let max_width = content_width.saturating_sub(4); + let wrapped = wrap_text_with_offsets(&msg.content, max_width); + let wrapped_count = wrapped.len(); + + for wrapped_line in wrapped.into_iter().take(2) { + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled(wrapped_line.text, Style::default().fg(msg_color)), + ])); + } + if wrapped_count > 2 { + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled("...", Style::default().fg(Color::Gray)), + ])); + } + } + } + + // Скролл к выбранному результату + let visible_height = chunks[1].height.saturating_sub(2) as usize; + let lines_per_result = 4; + let selected_line = app.selected_search_result_index * lines_per_result; + let scroll_offset = if selected_line > visible_height / 2 { + (selected_line - visible_height / 2) as u16 + } else { + 0 + }; + + let results_widget = Paragraph::new(lines) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Yellow)) + ) + .scroll((scroll_offset, 0)); + f.render_widget(results_widget, chunks[1]); + + // Help bar + let help_line = Line::from(vec![ + Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::raw("навигация"), + Span::raw(" "), + Span::styled(" n/N ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::raw("след./пред."), + Span::raw(" "), + Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::raw("перейти"), + Span::raw(" "), + Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), + Span::raw("выход"), + ]); + let help = Paragraph::new(help_line) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Yellow)) + ) + .alignment(Alignment::Center); + f.render_widget(help, chunks[2]); +} + /// Рендерит режим просмотра закреплённых сообщений fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { let chunks = Layout::default()