add find messages
This commit is contained in:
@@ -57,6 +57,15 @@ pub struct App {
|
||||
pub pinned_messages: Vec<crate::tdlib::client::MessageInfo>,
|
||||
/// Индекс выбранного 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<crate::tdlib::client::MessageInfo>,
|
||||
/// Индекс выбранного результата
|
||||
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<i64> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
&mut self,
|
||||
|
||||
@@ -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<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) {
|
||||
let chunks = Layout::default()
|
||||
|
||||
Reference in New Issue
Block a user