add find messages

This commit is contained in:
Mikhail Kilin
2026-01-27 12:09:05 +03:00
parent 81dc5b9007
commit dc76e01f3c
4 changed files with 318 additions and 0 deletions

View File

@@ -57,6 +57,15 @@ pub struct App {
pub pinned_messages: Vec<crate::tdlib::client::MessageInfo>, pub pinned_messages: Vec<crate::tdlib::client::MessageInfo>,
/// Индекс выбранного pinned сообщения /// Индекс выбранного pinned сообщения
pub selected_pinned_index: usize, pub selected_pinned_index: usize,
// Message search mode
/// Режим поиска по сообщениям
pub is_message_search_mode: bool,
/// Поисковый запрос
pub message_search_query: String,
/// Результаты поиска
pub message_search_results: Vec<crate::tdlib::client::MessageInfo>,
/// Индекс выбранного результата
pub selected_search_result_index: usize,
} }
impl App { impl App {
@@ -93,6 +102,10 @@ impl App {
is_pinned_mode: false, is_pinned_mode: false,
pinned_messages: Vec::new(), pinned_messages: Vec::new(),
selected_pinned_index: 0, 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.current_chat_messages.clear();
self.td_client.typing_status = None; self.td_client.typing_status = None;
self.td_client.current_pinned_message = 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<i64> { pub fn get_selected_pinned_id(&self) -> Option<i64> {
self.get_selected_pinned().map(|m| m.id) 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<crate::tdlib::client::MessageInfo>) {
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<i64> {
self.get_selected_search_result().map(|m| m.id)
}
} }

View File

@@ -49,9 +49,75 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
} }
return; 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() { if app.is_pinned_mode() {
match key.code { match key.code {

View File

@@ -1165,6 +1165,38 @@ impl TdClient {
} }
} }
/// Поиск сообщений в чате по тексту
pub async fn search_messages(&mut self, chat_id: i64, query: &str) -> Result<Vec<MessageInfo>, 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<MessageInfo> = 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( pub async fn load_older_messages(
&mut self, &mut self,

View File

@@ -307,6 +307,12 @@ fn adjust_entities_for_substring(
} }
pub fn render(f: &mut Frame, area: Rect, app: &App) { 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() { if app.is_pinned_mode() {
render_pinned_mode(f, area, app); 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<Line> = 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) { fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
let chunks = Layout::default() let chunks = Layout::default()