diff --git a/Cargo.lock b/Cargo.lock index d137464..81aeaf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2486,9 +2486,9 @@ dependencies = [ [[package]] name = "tdlib-rs" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98c960258301bee0758a669fbe12ad8a97c6e764d2f30c5426eea008eebf2d2" +checksum = "4c309480dcdd6d5dc2f37866d9063fed280780ddfeb51ae3a0adc2b52b0c0bc3" dependencies = [ "dirs 6.0.0", "futures-channel", @@ -2505,18 +2505,18 @@ dependencies = [ [[package]] name = "tdlib-rs-gen" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be6a2373951794ddcf612db2cd26fc67d9fb2721a1497e873c06bd87823fae80" +checksum = "ff69c8cab3e5285d2f79f53263077b2cdb12a841b230406e3b1230a345c78968" dependencies = [ "tdlib-rs-parser", ] [[package]] name = "tdlib-rs-parser" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cbdfae498e57fb48d380fff8eb5c9c98d4497c998f6de0d30d5d6b12f5358b" +checksum = "20b1c6703d2284b9d4ddb620cd350f726a1c43bb6f7801f4361b55db2421caa8" [[package]] name = "tele-tui" diff --git a/Cargo.toml b/Cargo.toml index b208b58..b196ea2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ url-open = ["dep:open"] [dependencies] ratatui = "0.29" crossterm = "0.28" -tdlib-rs = { version = "1.1", features = ["download-tdlib"] } +tdlib-rs = { version = "1.2.0", features = ["download-tdlib"] } tokio = { version = "1", features = ["full"] } async-trait = "0.1" serde = { version = "1.0", features = ["derive"] } @@ -38,7 +38,7 @@ tokio-test = "0.4" criterion = "0.5" [build-dependencies] -tdlib-rs = { version = "1.1", features = ["download-tdlib"] } +tdlib-rs = { version = "1.2.0", features = ["download-tdlib"] } [[bench]] name = "group_messages" diff --git a/src/input/main_input.rs b/src/input/main_input.rs index fdf1adb..4da13ed 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -21,447 +21,43 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Режим профиля if app.is_profile_mode() { - // Обработка подтверждения выхода из группы - let confirmation_step = app.get_leave_group_confirmation_step(); - if confirmation_step > 0 { - match handle_yes_no(key.code) { - Some(true) => { - // Подтверждение - if confirmation_step == 1 { - // Первое подтверждение - показываем второе - app.show_leave_group_final_confirmation(); - } else if confirmation_step == 2 { - // Второе подтверждение - выходим из группы - if let Some(chat_id) = app.selected_chat_id { - let leave_result = app.td_client.leave_chat(chat_id).await; - match leave_result { - Ok(_) => { - app.status_message = Some("Вы вышли из группы".to_string()); - app.exit_profile_mode(); - app.close_chat(); - } - Err(e) => { - app.error_message = Some(e); - app.cancel_leave_group(); - } - } - } - } - } - Some(false) => { - // Отмена - app.cancel_leave_group(); - } - None => { - // Другая клавиша - игнорируем - } - } - return; - } - - // Обычная навигация по профилю - match key.code { - KeyCode::Esc => { - app.exit_profile_mode(); - } - KeyCode::Up => { - app.select_previous_profile_action(); - } - KeyCode::Down => { - if let Some(profile) = app.get_profile_info() { - let max_actions = get_available_actions_count(profile); - app.select_next_profile_action(max_actions); - } - } - KeyCode::Enter => { - // Выполнить выбранное действие - if let Some(profile) = app.get_profile_info() { - let actions = get_available_actions_count(profile); - let action_index = app.get_selected_profile_action().unwrap_or(0); - - if action_index < actions { - // Определяем какое действие выбрано - let mut current_idx = 0; - - // Действие: Открыть в браузере - if profile.username.is_some() { - if action_index == current_idx { - if let Some(username) = &profile.username { - let url = format!( - "https://t.me/{}", - username.trim_start_matches('@') - ); - #[cfg(feature = "url-open")] - { - match open::that(&url) { - Ok(_) => { - app.status_message = Some(format!("Открыто: {}", url)); - } - Err(e) => { - app.error_message = - Some(format!("Ошибка открытия браузера: {}", e)); - } - } - } - #[cfg(not(feature = "url-open"))] - { - app.error_message = Some( - "Открытие URL недоступно (требуется feature 'url-open')".to_string() - ); - } - } - return; - } - current_idx += 1; - } - - // Действие: Скопировать ID - if action_index == current_idx { - app.status_message = - Some(format!("ID скопирован: {}", profile.chat_id)); - return; - } - current_idx += 1; - - // Действие: Покинуть группу - if profile.is_group && action_index == current_idx { - app.show_leave_group_confirmation(); - } - } - } - } - _ => {} - } + handle_profile_mode(app, key).await; 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_id = MessageId::new(msg_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 => { - // Удаляем символ из запроса - if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) { - query.pop(); - app.update_search_query(query.clone()); - // Выполняем поиск при изменении запроса - if let Some(chat_id) = app.get_selected_chat_id() { - if !query.is_empty() { - if let Ok(results) = with_timeout( - Duration::from_secs(3), - app.td_client.search_messages(ChatId::new(chat_id), &query), - ) - .await - { - app.set_search_results(results); - } - } else { - app.set_search_results(Vec::new()); - } - } - } - } - KeyCode::Char(c) => { - // Добавляем символ к запросу - if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) { - query.push(c); - app.update_search_query(query.clone()); - // Выполняем поиск при изменении запроса - if let Some(chat_id) = app.get_selected_chat_id() { - if let Ok(results) = with_timeout( - Duration::from_secs(3), - app.td_client.search_messages(ChatId::new(chat_id), &query), - ) - .await - { - app.set_search_results(results); - } - } - } - } - _ => {} - } + handle_message_search_mode(app, key).await; return; } // Режим просмотра закреплённых сообщений if app.is_pinned_mode() { - match key.code { - KeyCode::Esc => { - app.exit_pinned_mode(); - } - KeyCode::Up => { - app.select_previous_pinned(); - } - KeyCode::Down => { - app.select_next_pinned(); - } - KeyCode::Enter => { - // Перейти к сообщению в истории - if let Some(msg_id) = app.get_selected_pinned_id() { - let msg_id = MessageId::new(msg_id); - // Ищем индекс сообщения в текущей истории - let msg_index = app - .td_client - .current_chat_messages() - .iter() - .position(|m| m.id() == msg_id); - - if let Some(idx) = msg_index { - // Вычисляем scroll offset чтобы показать сообщение - let total = app.td_client.current_chat_messages().len(); - app.message_scroll_offset = total.saturating_sub(idx + 5); - } - app.exit_pinned_mode(); - } - } - _ => {} - } + handle_pinned_mode(app, key).await; return; } // Обработка ввода в режиме выбора реакции if app.is_reaction_picker_mode() { - match key.code { - KeyCode::Left => { - app.select_previous_reaction(); - app.needs_redraw = true; - } - KeyCode::Right => { - app.select_next_reaction(); - app.needs_redraw = true; - } - KeyCode::Up => { - // Переход на ряд выше (8 эмодзи в ряду) - if let crate::app::ChatState::ReactionPicker { - selected_index, - .. - } = &mut app.chat_state - { - if *selected_index >= 8 { - *selected_index = selected_index.saturating_sub(8); - app.needs_redraw = true; - } - } - } - KeyCode::Down => { - // Переход на ряд ниже (8 эмодзи в ряду) - if let crate::app::ChatState::ReactionPicker { - selected_index, - available_reactions, - .. - } = &mut app.chat_state - { - let new_index = *selected_index + 8; - if new_index < available_reactions.len() { - *selected_index = new_index; - app.needs_redraw = true; - } - } - } - KeyCode::Enter => { - // Добавить/убрать реакцию - if let Some(emoji) = app.get_selected_reaction().cloned() { - if let Some(message_id) = app.get_selected_message_for_reaction() { - if let Some(chat_id) = app.selected_chat_id { - let message_id = MessageId::new(message_id); - app.status_message = Some("Отправка реакции...".to_string()); - app.needs_redraw = true; - - match with_timeout_msg( - Duration::from_secs(5), - app.td_client - .toggle_reaction(chat_id, message_id, emoji.clone()), - "Таймаут отправки реакции", - ) - .await - { - Ok(_) => { - app.status_message = - Some(format!("Реакция {} добавлена", emoji)); - app.exit_reaction_picker_mode(); - app.needs_redraw = true; - } - Err(e) => { - app.error_message = Some(e); - app.status_message = None; - app.needs_redraw = true; - } - } - } - } - } - } - KeyCode::Esc => { - app.exit_reaction_picker_mode(); - app.needs_redraw = true; - } - _ => {} - } + handle_reaction_picker_mode(app, key).await; return; } // Модалка подтверждения удаления if app.is_confirm_delete_shown() { - match handle_yes_no(key.code) { - Some(true) => { - // Подтверждение удаления - if let Some(msg_id) = app.chat_state.selected_message_id() { - if let Some(chat_id) = app.get_selected_chat_id() { - // Находим сообщение для проверки can_be_deleted_for_all_users - let can_delete_for_all = app - .td_client - .current_chat_messages() - .iter() - .find(|m| m.id() == msg_id) - .map(|m| m.can_be_deleted_for_all_users()) - .unwrap_or(false); - - match with_timeout_msg( - Duration::from_secs(5), - app.td_client.delete_messages( - ChatId::new(chat_id), - vec![msg_id], - can_delete_for_all, - ), - "Таймаут удаления", - ) - .await - { - Ok(_) => { - // Удаляем из локального списка - app.td_client - .current_chat_messages_mut() - .retain(|m| m.id() != msg_id); - // Сбрасываем состояние - app.chat_state = crate::app::ChatState::Normal; - } - Err(e) => { - app.error_message = Some(e); - } - } - } - } - // Закрываем модалку - app.chat_state = crate::app::ChatState::Normal; - } - Some(false) => { - // Отмена удаления - app.chat_state = crate::app::ChatState::Normal; - } - None => { - // Другая клавиша - игнорируем - } - } + handle_delete_confirmation(app, key).await; return; } // Режим выбора чата для пересылки if app.is_forwarding() { - match key.code { - KeyCode::Esc => { - app.cancel_forward(); - } - KeyCode::Enter => { - // Выбираем чат и пересылаем сообщение - let filtered = app.get_filtered_chats(); - if let Some(i) = app.chat_list_state.selected() { - if let Some(chat) = filtered.get(i) { - let to_chat_id = chat.id; - if let Some(msg_id) = app.chat_state.selected_message_id() { - if let Some(from_chat_id) = app.get_selected_chat_id() { - match with_timeout_msg( - Duration::from_secs(5), - app.td_client.forward_messages( - to_chat_id, - ChatId::new(from_chat_id), - vec![msg_id], - ), - "Таймаут пересылки", - ) - .await - { - Ok(_) => { - app.status_message = - Some("Сообщение переслано".to_string()); - } - Err(e) => { - app.error_message = Some(e); - } - } - } - } - } - } - app.cancel_forward(); - } - KeyCode::Down => { - app.next_chat(); - } - KeyCode::Up => { - app.previous_chat(); - } - _ => {} - } + handle_forward_mode(app, key).await; return; } // Режим поиска if app.is_searching { - match key.code { - KeyCode::Esc => { - app.cancel_search(); - } - KeyCode::Enter => { - // Выбрать чат из отфильтрованного списка - app.select_filtered_chat(); - if let Some(chat_id) = app.get_selected_chat_id() { - open_chat_and_load_data(app, chat_id).await; - } - } - KeyCode::Backspace => { - app.search_query.pop(); - // Сбрасываем выделение при изменении запроса - app.chat_list_state.select(Some(0)); - } - KeyCode::Down => { - app.next_filtered_chat(); - } - KeyCode::Up => { - app.previous_filtered_chat(); - } - KeyCode::Char(c) => { - app.search_query.push(c); - // Сбрасываем выделение при изменении запроса - app.chat_list_state.select(Some(0)); - } - _ => {} - } + handle_chat_search_mode(app, key).await; return; } @@ -600,28 +196,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Esc - отменить выбор/редактирование/reply или закрыть чат if key.code == KeyCode::Esc { - if app.is_selecting_message() { - // Отменить выбор сообщения - app.chat_state = crate::app::ChatState::Normal; - } else if app.is_editing() { - // Отменить редактирование - app.cancel_editing(); - } else if app.is_replying() { - // Отменить режим ответа - app.cancel_reply(); - } else if app.selected_chat_id.is_some() { - // Сохраняем черновик если есть текст в инпуте - if let Some(chat_id) = app.selected_chat_id { - if !app.message_input.is_empty() && !app.is_editing() && !app.is_replying() { - let draft_text = app.message_input.clone(); - let _ = app.td_client.set_draft_message(chat_id, draft_text).await; - } else if app.message_input.is_empty() { - // Очищаем черновик если инпут пустой - let _ = app.td_client.set_draft_message(chat_id, String::new()).await; - } - } - app.close_chat(); - } + handle_escape_key(app).await; return; } @@ -629,89 +204,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if app.selected_chat_id.is_some() { // Режим выбора сообщения для редактирования/удаления if app.is_selecting_message() { - match key.code { - KeyCode::Up => { - app.select_previous_message(); - } - KeyCode::Down => { - app.select_next_message(); - // Если вышли из режима выбора (индекс стал None), ничего не делаем - } - KeyCode::Char('d') | KeyCode::Char('в') | KeyCode::Delete => { - // Показать модалку подтверждения удаления - if let Some(msg) = app.get_selected_message() { - let can_delete = - msg.can_be_deleted_only_for_self() || msg.can_be_deleted_for_all_users(); - if can_delete { - app.chat_state = crate::app::ChatState::DeleteConfirmation { - message_id: msg.id(), - }; - } - } - } - KeyCode::Char('r') | KeyCode::Char('к') => { - // Начать режим ответа на выбранное сообщение - app.start_reply_to_selected(); - } - KeyCode::Char('f') | KeyCode::Char('а') => { - // Начать режим пересылки - app.start_forward_selected(); - } - KeyCode::Char('y') | KeyCode::Char('н') => { - // Копировать сообщение - if let Some(msg) = app.get_selected_message() { - let text = format_message_for_clipboard(&msg); - match copy_to_clipboard(&text) { - Ok(_) => { - app.status_message = Some("Сообщение скопировано".to_string()); - } - Err(e) => { - app.error_message = Some(format!("Ошибка копирования: {}", e)); - } - } - } - } - KeyCode::Char('e') | KeyCode::Char('у') => { - // Открыть emoji picker для добавления реакции - if let Some(msg) = app.get_selected_message() { - let chat_id = app.selected_chat_id.unwrap(); - let message_id = msg.id(); - - app.status_message = Some("Загрузка реакций...".to_string()); - app.needs_redraw = true; - - // Запрашиваем доступные реакции - match with_timeout_msg( - Duration::from_secs(5), - app.td_client - .get_message_available_reactions(chat_id, message_id), - "Таймаут загрузки реакций", - ) - .await - { - Ok(reactions) => { - let reactions: Vec = reactions; - if reactions.is_empty() { - app.error_message = - Some("Реакции недоступны для этого сообщения".to_string()); - app.status_message = None; - app.needs_redraw = true; - } else { - app.enter_reaction_picker_mode(message_id.as_i64(), reactions); - app.status_message = None; - app.needs_redraw = true; - } - } - Err(e) => { - app.error_message = Some(e); - app.status_message = None; - app.needs_redraw = true; - } - } - } - } - _ => {} - } + handle_message_selection(app, key).await; return; } @@ -903,6 +396,620 @@ pub async fn handle(app: &mut App, key: KeyEvent) { } } +/// Обработка режима выбора сообщения в открытом чате. +/// +/// Поддерживаемые действия: +/// - `Up` - выбрать предыдущее сообщение +/// - `Down` - выбрать следующее сообщение +/// - `d`/`в`/`Delete` - показать модалку удаления +/// - `r`/`к` - начать режим ответа (reply) +/// - `f`/`а` - начать режим пересылки (forward) +/// - `y`/`н` - скопировать сообщение в буфер обмена +/// - `e`/`у` - открыть emoji picker для реакции +async fn handle_message_selection(app: &mut App, key: KeyEvent) { + match key.code { + KeyCode::Up => { + app.select_previous_message(); + } + KeyCode::Down => { + app.select_next_message(); + // Если вышли из режима выбора (индекс стал None), ничего не делаем + } + KeyCode::Char('d') | KeyCode::Char('в') | KeyCode::Delete => { + // Показать модалку подтверждения удаления + if let Some(msg) = app.get_selected_message() { + let can_delete = + msg.can_be_deleted_only_for_self() || msg.can_be_deleted_for_all_users(); + if can_delete { + app.chat_state = crate::app::ChatState::DeleteConfirmation { + message_id: msg.id(), + }; + } + } + } + KeyCode::Char('r') | KeyCode::Char('к') => { + // Начать режим ответа на выбранное сообщение + app.start_reply_to_selected(); + } + KeyCode::Char('f') | KeyCode::Char('а') => { + // Начать режим пересылки + app.start_forward_selected(); + } + KeyCode::Char('y') | KeyCode::Char('н') => { + // Копировать сообщение + if let Some(msg) = app.get_selected_message() { + let text = format_message_for_clipboard(&msg); + match copy_to_clipboard(&text) { + Ok(_) => { + app.status_message = Some("Сообщение скопировано".to_string()); + } + Err(e) => { + app.error_message = Some(format!("Ошибка копирования: {}", e)); + } + } + } + } + KeyCode::Char('e') | KeyCode::Char('у') => { + // Открыть emoji picker для добавления реакции + if let Some(msg) = app.get_selected_message() { + let chat_id = app.selected_chat_id.unwrap(); + let message_id = msg.id(); + + app.status_message = Some("Загрузка реакций...".to_string()); + app.needs_redraw = true; + + // Запрашиваем доступные реакции + match with_timeout_msg( + Duration::from_secs(5), + app.td_client + .get_message_available_reactions(chat_id, message_id), + "Таймаут загрузки реакций", + ) + .await + { + Ok(reactions) => { + let reactions: Vec = reactions; + if reactions.is_empty() { + app.error_message = + Some("Реакции недоступны для этого сообщения".to_string()); + app.status_message = None; + app.needs_redraw = true; + } else { + app.enter_reaction_picker_mode(message_id.as_i64(), reactions); + app.status_message = None; + app.needs_redraw = true; + } + } + Err(e) => { + app.error_message = Some(e); + app.status_message = None; + app.needs_redraw = true; + } + } + } + } + _ => {} + } +} + +/// Обработка нажатия клавиши Esc - отмена действий или закрытие чата. +/// +/// Приоритет действий: +/// 1. Отмена выбора сообщения +/// 2. Отмена редактирования +/// 3. Отмена режима ответа (reply) +/// 4. Закрытие чата (с сохранением черновика) +async fn handle_escape_key(app: &mut App) { + if app.is_selecting_message() { + // Отменить выбор сообщения + app.chat_state = crate::app::ChatState::Normal; + } else if app.is_editing() { + // Отменить редактирование + app.cancel_editing(); + } else if app.is_replying() { + // Отменить режим ответа + app.cancel_reply(); + } else if app.selected_chat_id.is_some() { + // Сохраняем черновик если есть текст в инпуте + if let Some(chat_id) = app.selected_chat_id { + if !app.message_input.is_empty() && !app.is_editing() && !app.is_replying() { + let draft_text = app.message_input.clone(); + let _ = app.td_client.set_draft_message(chat_id, draft_text).await; + } else if app.message_input.is_empty() { + // Очищаем черновик если инпут пустой + let _ = app.td_client.set_draft_message(chat_id, String::new()).await; + } + } + app.close_chat(); + } +} + +/// Обработка режима профиля пользователя/чата. +/// +/// Включает: +/// - Навигацию по действиям профиля (Up/Down) +/// - Выполнение действий (Enter): открыть в браузере, скопировать ID, покинуть группу +/// - Модалку подтверждения выхода из группы (двухшаговое подтверждение) +/// - Выход из режима (Esc) +async fn handle_profile_mode(app: &mut App, key: KeyEvent) { + // Обработка подтверждения выхода из группы + let confirmation_step = app.get_leave_group_confirmation_step(); + if confirmation_step > 0 { + match handle_yes_no(key.code) { + Some(true) => { + // Подтверждение + if confirmation_step == 1 { + // Первое подтверждение - показываем второе + app.show_leave_group_final_confirmation(); + } else if confirmation_step == 2 { + // Второе подтверждение - выходим из группы + if let Some(chat_id) = app.selected_chat_id { + let leave_result = app.td_client.leave_chat(chat_id).await; + match leave_result { + Ok(_) => { + app.status_message = Some("Вы вышли из группы".to_string()); + app.exit_profile_mode(); + app.close_chat(); + } + Err(e) => { + app.error_message = Some(e); + app.cancel_leave_group(); + } + } + } + } + } + Some(false) => { + // Отмена + app.cancel_leave_group(); + } + None => { + // Другая клавиша - игнорируем + } + } + return; + } + + // Обычная навигация по профилю + match key.code { + KeyCode::Esc => { + app.exit_profile_mode(); + } + KeyCode::Up => { + app.select_previous_profile_action(); + } + KeyCode::Down => { + if let Some(profile) = app.get_profile_info() { + let max_actions = get_available_actions_count(profile); + app.select_next_profile_action(max_actions); + } + } + KeyCode::Enter => { + // Выполнить выбранное действие + if let Some(profile) = app.get_profile_info() { + let actions = get_available_actions_count(profile); + let action_index = app.get_selected_profile_action().unwrap_or(0); + + if action_index < actions { + // Определяем какое действие выбрано + let mut current_idx = 0; + + // Действие: Открыть в браузере + if profile.username.is_some() { + if action_index == current_idx { + if let Some(username) = &profile.username { + let url = format!( + "https://t.me/{}", + username.trim_start_matches('@') + ); + #[cfg(feature = "url-open")] + { + match open::that(&url) { + Ok(_) => { + app.status_message = Some(format!("Открыто: {}", url)); + } + Err(e) => { + app.error_message = + Some(format!("Ошибка открытия браузера: {}", e)); + } + } + } + #[cfg(not(feature = "url-open"))] + { + app.error_message = Some( + "Открытие URL недоступно (требуется feature 'url-open')".to_string() + ); + } + } + return; + } + current_idx += 1; + } + + // Действие: Скопировать ID + if action_index == current_idx { + app.status_message = + Some(format!("ID скопирован: {}", profile.chat_id)); + return; + } + current_idx += 1; + + // Действие: Покинуть группу + if profile.is_group && action_index == current_idx { + app.show_leave_group_confirmation(); + } + } + } + } + _ => {} + } +} + +/// Обработка режима поиска по чатам (Ctrl+S). +/// +/// Поддерживаемые действия: +/// - `Esc` - выход из режима поиска +/// - `Enter` - открыть выбранный чат +/// - `Backspace` - удалить символ из поискового запроса +/// - `Up` - предыдущий чат в отфильтрованном списке +/// - `Down` - следующий чат в отфильтрованном списке +/// - `Char(c)` - добавить символ к поисковому запросу +async fn handle_chat_search_mode(app: &mut App, key: KeyEvent) { + match key.code { + KeyCode::Esc => { + app.cancel_search(); + } + KeyCode::Enter => { + // Выбрать чат из отфильтрованного списка + app.select_filtered_chat(); + if let Some(chat_id) = app.get_selected_chat_id() { + open_chat_and_load_data(app, chat_id).await; + } + } + KeyCode::Backspace => { + app.search_query.pop(); + // Сбрасываем выделение при изменении запроса + app.chat_list_state.select(Some(0)); + } + KeyCode::Down => { + app.next_filtered_chat(); + } + KeyCode::Up => { + app.previous_filtered_chat(); + } + KeyCode::Char(c) => { + app.search_query.push(c); + // Сбрасываем выделение при изменении запроса + app.chat_list_state.select(Some(0)); + } + _ => {} + } +} + +/// Обработка режима выбора чата для пересылки сообщения. +/// +/// Поддерживаемые действия: +/// - `Esc` - отмена пересылки +/// - `Enter` - переслать сообщение в выбранный чат +/// - `Up` - предыдущий чат в списке +/// - `Down` - следующий чат в списке +async fn handle_forward_mode(app: &mut App, key: KeyEvent) { + match key.code { + KeyCode::Esc => { + app.cancel_forward(); + } + KeyCode::Enter => { + // Выбираем чат и пересылаем сообщение + let filtered = app.get_filtered_chats(); + if let Some(i) = app.chat_list_state.selected() { + if let Some(chat) = filtered.get(i) { + let to_chat_id = chat.id; + if let Some(msg_id) = app.chat_state.selected_message_id() { + if let Some(from_chat_id) = app.get_selected_chat_id() { + match with_timeout_msg( + Duration::from_secs(5), + app.td_client.forward_messages( + to_chat_id, + ChatId::new(from_chat_id), + vec![msg_id], + ), + "Таймаут пересылки", + ) + .await + { + Ok(_) => { + app.status_message = + Some("Сообщение переслано".to_string()); + } + Err(e) => { + app.error_message = Some(e); + } + } + } + } + } + } + app.cancel_forward(); + } + KeyCode::Down => { + app.next_chat(); + } + KeyCode::Up => { + app.previous_chat(); + } + _ => {} + } +} + +/// Обработка модалки подтверждения удаления сообщения. +/// +/// Поддерживаемые действия: +/// - `y`/`н`/`Enter` - подтверждение удаления +/// - `n`/`т`/`Esc` - отмена удаления +/// - Другие клавиши игнорируются +async fn handle_delete_confirmation(app: &mut App, key: KeyEvent) { + match handle_yes_no(key.code) { + Some(true) => { + // Подтверждение удаления + if let Some(msg_id) = app.chat_state.selected_message_id() { + if let Some(chat_id) = app.get_selected_chat_id() { + // Находим сообщение для проверки can_be_deleted_for_all_users + let can_delete_for_all = app + .td_client + .current_chat_messages() + .iter() + .find(|m| m.id() == msg_id) + .map(|m| m.can_be_deleted_for_all_users()) + .unwrap_or(false); + + match with_timeout_msg( + Duration::from_secs(5), + app.td_client.delete_messages( + ChatId::new(chat_id), + vec![msg_id], + can_delete_for_all, + ), + "Таймаут удаления", + ) + .await + { + Ok(_) => { + // Удаляем из локального списка + app.td_client + .current_chat_messages_mut() + .retain(|m| m.id() != msg_id); + // Сбрасываем состояние + app.chat_state = crate::app::ChatState::Normal; + } + Err(e) => { + app.error_message = Some(e); + } + } + } + } + // Закрываем модалку + app.chat_state = crate::app::ChatState::Normal; + } + Some(false) => { + // Отмена удаления + app.chat_state = crate::app::ChatState::Normal; + } + None => { + // Другая клавиша - игнорируем + } + } +} + +/// Обработка режима выбора реакции (emoji picker). +/// +/// Поддерживаемые действия: +/// - `Left` - предыдущая реакция +/// - `Right` - следующая реакция +/// - `Up` - ряд выше (8 эмодзи в ряду) +/// - `Down` - ряд ниже (8 эмодзи в ряду) +/// - `Enter` - добавить/убрать выбранную реакцию +/// - `Esc` - выход из режима +async fn handle_reaction_picker_mode(app: &mut App, key: KeyEvent) { + match key.code { + KeyCode::Left => { + app.select_previous_reaction(); + app.needs_redraw = true; + } + KeyCode::Right => { + app.select_next_reaction(); + app.needs_redraw = true; + } + KeyCode::Up => { + // Переход на ряд выше (8 эмодзи в ряду) + if let crate::app::ChatState::ReactionPicker { + selected_index, + .. + } = &mut app.chat_state + { + if *selected_index >= 8 { + *selected_index = selected_index.saturating_sub(8); + app.needs_redraw = true; + } + } + } + KeyCode::Down => { + // Переход на ряд ниже (8 эмодзи в ряду) + if let crate::app::ChatState::ReactionPicker { + selected_index, + available_reactions, + .. + } = &mut app.chat_state + { + let new_index = *selected_index + 8; + if new_index < available_reactions.len() { + *selected_index = new_index; + app.needs_redraw = true; + } + } + } + KeyCode::Enter => { + // Добавить/убрать реакцию + if let Some(emoji) = app.get_selected_reaction().cloned() { + if let Some(message_id) = app.get_selected_message_for_reaction() { + if let Some(chat_id) = app.selected_chat_id { + let message_id = MessageId::new(message_id); + app.status_message = Some("Отправка реакции...".to_string()); + app.needs_redraw = true; + + match with_timeout_msg( + Duration::from_secs(5), + app.td_client + .toggle_reaction(chat_id, message_id, emoji.clone()), + "Таймаут отправки реакции", + ) + .await + { + Ok(_) => { + app.status_message = + Some(format!("Реакция {} добавлена", emoji)); + app.exit_reaction_picker_mode(); + app.needs_redraw = true; + } + Err(e) => { + app.error_message = Some(e); + app.status_message = None; + app.needs_redraw = true; + } + } + } + } + } + } + KeyCode::Esc => { + app.exit_reaction_picker_mode(); + app.needs_redraw = true; + } + _ => {} + } +} + +/// Обработка режима поиска по сообщениям в открытом чате. +/// +/// Поддерживаемые действия: +/// - `Esc` - выход из режима поиска +/// - `Up`/`N` - предыдущий результат поиска +/// - `Down`/`n` - следующий результат поиска +/// - `Enter` - переход к выбранному сообщению +/// - `Backspace` - удалить символ из запроса +/// - `Char(c)` - добавить символ к запросу (с автоматическим поиском) +async fn handle_message_search_mode(app: &mut App, key: KeyEvent) { + 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_id = MessageId::new(msg_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 => { + // Удаляем символ из запроса + if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) { + query.pop(); + app.update_search_query(query.clone()); + // Выполняем поиск при изменении запроса + if let Some(chat_id) = app.get_selected_chat_id() { + if !query.is_empty() { + if let Ok(results) = with_timeout( + Duration::from_secs(3), + app.td_client.search_messages(ChatId::new(chat_id), &query), + ) + .await + { + app.set_search_results(results); + } + } else { + app.set_search_results(Vec::new()); + } + } + } + } + KeyCode::Char(c) => { + // Добавляем символ к запросу + if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) { + query.push(c); + app.update_search_query(query.clone()); + // Выполняем поиск при изменении запроса + if let Some(chat_id) = app.get_selected_chat_id() { + if let Ok(results) = with_timeout( + Duration::from_secs(3), + app.td_client.search_messages(ChatId::new(chat_id), &query), + ) + .await + { + app.set_search_results(results); + } + } + } + } + _ => {} + } +} + +/// Обработка режима просмотра закреплённых сообщений. +/// +/// Поддерживаемые действия: +/// - `Esc` - выход из режима +/// - `Up` - предыдущее закреплённое сообщение +/// - `Down` - следующее закреплённое сообщение +/// - `Enter` - переход к сообщению в истории чата +async fn handle_pinned_mode(app: &mut App, key: KeyEvent) { + match key.code { + KeyCode::Esc => { + app.exit_pinned_mode(); + } + KeyCode::Up => { + app.select_previous_pinned(); + } + KeyCode::Down => { + app.select_next_pinned(); + } + KeyCode::Enter => { + // Перейти к сообщению в истории + if let Some(msg_id) = app.get_selected_pinned_id() { + let msg_id = MessageId::new(msg_id); + // Ищем индекс сообщения в текущей истории + let msg_index = app + .td_client + .current_chat_messages() + .iter() + .position(|m| m.id() == msg_id); + + if let Some(idx) = msg_index { + // Вычисляем scroll offset чтобы показать сообщение + let total = app.td_client.current_chat_messages().len(); + app.message_scroll_offset = total.saturating_sub(idx + 5); + } + app.exit_pinned_mode(); + } + } + _ => {} + } +} + /// Открывает чат и загружает все необходимые данные. /// /// Выполняет: