add find messages
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user