From 45d03b59fd674b901a369d4c96202560154905c3 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Tue, 3 Feb 2026 17:15:47 +0300 Subject: [PATCH] refactor: complete main_input.rs simplification (Phase 3/3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Извлечены все оставшиеся блоки из функции handle(): - handle_profile_mode() - режим профиля с модалкой (~120 строк) - handle_message_search_mode() - поиск по сообщениям (~73 строки) - handle_pinned_mode() - закреплённые сообщения (~42 строки) - handle_reaction_picker_mode() - emoji picker (~90 строк) - handle_delete_confirmation() - подтверждение удаления (~60 строк) - handle_forward_mode() - пересылка сообщений (~52 строки) - handle_chat_search_mode() - поиск по чатам (~43 строки) - handle_enter_key() - обработка Enter (~145 строк) - handle_escape_key() - обработка Esc (~35 строк) - handle_message_selection() - режим выбора сообщения (~95 строк) - handle_profile_open() - Ctrl+U для профиля (~28 строк) Результат: - Функция handle() сокращена с 734 до 82 строк (89% сокращение!) - Всего извлечено 13 специализированных функций - Каждая функция имеет чёткую ответственность - Код стал линейным и легко читаемым Co-Authored-By: Claude Sonnet 4.5 --- src/input/main_input.rs | 1440 +++++++++++++++++++++------------------ 1 file changed, 777 insertions(+), 663 deletions(-) diff --git a/src/input/main_input.rs b/src/input/main_input.rs index 79fdbe5..3cc3935 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -11,6 +11,772 @@ use crate::utils::modal_handler::handle_yes_no; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::time::{Duration, Instant}; +/// Обработка режима профиля пользователя/чата +/// +/// Обрабатывает: +/// - Модалку подтверждения выхода из группы (двухшаговая) +/// - Навигацию по действиям профиля (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+U для открытия профиля чата/пользователя +/// +/// Загружает информацию о профиле и переключает в режим просмотра профиля +async fn handle_profile_open(app: &mut App) { + if let Some(chat_id) = app.selected_chat_id { + app.status_message = Some("Загрузка профиля...".to_string()); + match with_timeout_msg( + Duration::from_secs(5), + app.td_client.get_profile_info(chat_id), + "Таймаут загрузки профиля", + ) + .await + { + Ok(profile) => { + app.enter_profile_mode(profile); + app.status_message = None; + } + Err(e) => { + app.error_message = Some(e); + app.status_message = None; + } + } + } +} + +/// Обработка режима выбора сообщения для действий +/// +/// Обрабатывает: +/// - Навигацию по сообщениям (Up/Down) +/// - Удаление сообщения (d/в/Delete) +/// - Ответ на сообщение (r/к) +/// - Пересылку сообщения (f/а) +/// - Копирование сообщения (y/н) +/// - Добавление реакции (e/у) +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 +/// +/// Обрабатывает отмену текущего действия или закрытие чата: +/// - В режиме выбора сообщения: отменить выбор +/// - В режиме редактирования: отменить редактирование +/// - В режиме ответа: отменить ответ +/// - В открытом чате: сохранить черновик и закрыть чат +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(); + } +} + +/// Обработка клавиши Enter +/// +/// Обрабатывает три сценария: +/// 1. В режиме выбора сообщения: начать редактирование +/// 2. В открытом чате: отправить новое или редактировать существующее сообщение +/// 3. В списке чатов: открыть выбранный чат +async fn handle_enter_key(app: &mut App) { + if app.selected_chat_id.is_some() { + // Режим выбора сообщения + if app.is_selecting_message() { + // Начать редактирование выбранного сообщения + if app.start_editing_selected() { + // Редактирование начато + } else { + // Нельзя редактировать это сообщение + app.chat_state = crate::app::ChatState::Normal; + } + return; + } + + // Отправка или редактирование сообщения + if is_non_empty(&app.message_input) { + if let Some(chat_id) = app.get_selected_chat_id() { + let text = app.message_input.clone(); + + if app.is_editing() { + // Режим редактирования + if let Some(msg_id) = app.chat_state.selected_message_id() { + // Проверяем, что сообщение есть в локальном кэше + let msg_exists = app.td_client.current_chat_messages() + .iter() + .any(|m| m.id() == msg_id); + + if !msg_exists { + app.error_message = Some(format!( + "Сообщение {} не найдено в кэше чата {}", + msg_id.as_i64(), chat_id + )); + app.chat_state = crate::app::ChatState::Normal; + app.message_input.clear(); + app.cursor_position = 0; + return; + } + + match with_timeout_msg( + Duration::from_secs(5), + app.td_client.edit_message(ChatId::new(chat_id), msg_id, text), + "Таймаут редактирования", + ) + .await + { + Ok(mut edited_msg) => { + // Сохраняем reply_to из старого сообщения (если есть) + let messages = app.td_client.current_chat_messages_mut(); + if let Some(pos) = messages.iter().position(|m| m.id() == msg_id) { + let old_reply_to = messages[pos].interactions.reply_to.clone(); + // Если в старом сообщении был reply и в новом он "Unknown" - сохраняем старый + if let Some(old_reply) = old_reply_to { + if edited_msg.interactions.reply_to.as_ref() + .map_or(true, |r| r.sender_name == "Unknown") { + edited_msg.interactions.reply_to = Some(old_reply); + } + } + // Заменяем сообщение + messages[pos] = edited_msg; + } + // Очищаем инпут и сбрасываем состояние ПОСЛЕ успешного редактирования + app.message_input.clear(); + app.cursor_position = 0; + app.chat_state = crate::app::ChatState::Normal; + app.needs_redraw = true; // ВАЖНО: перерисовываем UI + } + Err(e) => { + app.error_message = Some(e); + } + } + } + } else { + // Обычная отправка (или reply) + let reply_to_id = if app.is_replying() { + app.chat_state.selected_message_id() + } else { + None + }; + // Создаём ReplyInfo ДО отправки, пока сообщение точно доступно + let reply_info = app.get_replying_to_message().map(|m| { + crate::tdlib::ReplyInfo { + message_id: m.id(), + sender_name: m.sender_name().to_string(), + text: m.text().to_string(), + } + }); + app.message_input.clear(); + app.cursor_position = 0; + // Сбрасываем режим reply если он был активен + if app.is_replying() { + app.chat_state = crate::app::ChatState::Normal; + } + app.last_typing_sent = None; + + // Отменяем typing status + app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Cancel).await; + + match with_timeout_msg( + Duration::from_secs(5), + app.td_client + .send_message(ChatId::new(chat_id), text, reply_to_id, reply_info), + "Таймаут отправки", + ) + .await + { + Ok(sent_msg) => { + // Добавляем отправленное сообщение в список (с лимитом) + app.td_client.push_message(sent_msg); + // Сбрасываем скролл чтобы видеть новое сообщение + app.message_scroll_offset = 0; + } + Err(e) => { + app.error_message = Some(e); + } + } + } + } + } + } else { + // Открываем чат + let prev_selected = app.selected_chat_id; + app.select_current_chat(); + + if app.selected_chat_id != prev_selected { + if let Some(chat_id) = app.get_selected_chat_id() { + open_chat_and_load_data(app, chat_id).await; + } + } + } +} + +/// Обработка режима поиска по чатам (Ctrl+S) +/// +/// Обрабатывает: +/// - Редактирование поискового запроса (Backspace, Char) +/// - Навигацию по отфильтрованному списку (Up/Down) +/// - Открытие выбранного чата (Enter) +/// - Отмену поиска (Esc) +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)); + } + _ => {} + } +} + +/// Обработка режима выбора чата для пересылки сообщения +/// +/// Обрабатывает: +/// - Навигацию по списку чатов (Up/Down) +/// - Пересылку сообщения в выбранный чат (Enter) +/// - Отмену пересылки (Esc) +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/y/Д/д) +/// - Отмена удаления (N/n/Т/т) +/// - Удаление для себя или для всех (зависит от can_be_deleted_for_all_users) +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/Down (сетка 8x6) +/// - Добавление/удаление реакции (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; + } + _ => {} + } +} + +/// Обработка режима просмотра закреплённых сообщений +/// +/// Обрабатывает: +/// - Навигацию по закреплённым сообщениям (Up/Down) +/// - Переход к сообщению в истории (Enter) +/// - Выход из режима (Esc) +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(); + } + } + _ => {} + } +} + +/// Обработка режима поиска по сообщениям в открытом чате +/// +/// Обрабатывает: +/// - Навигацию по результатам поиска (Up/Down/N/n) +/// - Переход к выбранному сообщению (Enter) +/// - Редактирование поискового запроса (Backspace, Char) +/// - Выход из режима поиска (Esc) +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); + } + } + } + } + _ => {} + } +} + /// Обработка навигации в списке чатов /// /// Обрабатывает: @@ -198,607 +964,55 @@ 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; } // Enter - открыть чат, отправить сообщение или редактировать if key.code == KeyCode::Enter { - if app.selected_chat_id.is_some() { - // Режим выбора сообщения - if app.is_selecting_message() { - // Начать редактирование выбранного сообщения - if app.start_editing_selected() { - // Редактирование начато - } else { - // Нельзя редактировать это сообщение - app.chat_state = crate::app::ChatState::Normal; - } - return; - } - - // Отправка или редактирование сообщения - if is_non_empty(&app.message_input) { - if let Some(chat_id) = app.get_selected_chat_id() { - let text = app.message_input.clone(); - - if app.is_editing() { - // Режим редактирования - if let Some(msg_id) = app.chat_state.selected_message_id() { - // Проверяем, что сообщение есть в локальном кэше - let msg_exists = app.td_client.current_chat_messages() - .iter() - .any(|m| m.id() == msg_id); - - if !msg_exists { - app.error_message = Some(format!( - "Сообщение {} не найдено в кэше чата {}", - msg_id.as_i64(), chat_id - )); - app.chat_state = crate::app::ChatState::Normal; - app.message_input.clear(); - app.cursor_position = 0; - return; - } - - match with_timeout_msg( - Duration::from_secs(5), - app.td_client.edit_message(ChatId::new(chat_id), msg_id, text), - "Таймаут редактирования", - ) - .await - { - Ok(mut edited_msg) => { - // Сохраняем reply_to из старого сообщения (если есть) - let messages = app.td_client.current_chat_messages_mut(); - if let Some(pos) = messages.iter().position(|m| m.id() == msg_id) { - let old_reply_to = messages[pos].interactions.reply_to.clone(); - // Если в старом сообщении был reply и в новом он "Unknown" - сохраняем старый - if let Some(old_reply) = old_reply_to { - if edited_msg.interactions.reply_to.as_ref() - .map_or(true, |r| r.sender_name == "Unknown") { - edited_msg.interactions.reply_to = Some(old_reply); - } - } - // Заменяем сообщение - messages[pos] = edited_msg; - } - // Очищаем инпут и сбрасываем состояние ПОСЛЕ успешного редактирования - app.message_input.clear(); - app.cursor_position = 0; - app.chat_state = crate::app::ChatState::Normal; - app.needs_redraw = true; // ВАЖНО: перерисовываем UI - } - Err(e) => { - app.error_message = Some(e); - } - } - } - } else { - // Обычная отправка (или reply) - let reply_to_id = if app.is_replying() { - app.chat_state.selected_message_id() - } else { - None - }; - // Создаём ReplyInfo ДО отправки, пока сообщение точно доступно - let reply_info = app.get_replying_to_message().map(|m| { - crate::tdlib::ReplyInfo { - message_id: m.id(), - sender_name: m.sender_name().to_string(), - text: m.text().to_string(), - } - }); - app.message_input.clear(); - app.cursor_position = 0; - // Сбрасываем режим reply если он был активен - if app.is_replying() { - app.chat_state = crate::app::ChatState::Normal; - } - app.last_typing_sent = None; - - // Отменяем typing status - app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Cancel).await; - - match with_timeout_msg( - Duration::from_secs(5), - app.td_client - .send_message(ChatId::new(chat_id), text, reply_to_id, reply_info), - "Таймаут отправки", - ) - .await - { - Ok(sent_msg) => { - // Добавляем отправленное сообщение в список (с лимитом) - app.td_client.push_message(sent_msg); - // Сбрасываем скролл чтобы видеть новое сообщение - app.message_scroll_offset = 0; - } - Err(e) => { - app.error_message = Some(e); - } - } - } - } - } - } else { - // Открываем чат - let prev_selected = app.selected_chat_id; - app.select_current_chat(); - - if app.selected_chat_id != prev_selected { - if let Some(chat_id) = app.get_selected_chat_id() { - open_chat_and_load_data(app, chat_id).await; - } - } - } + handle_enter_key(app).await; return; } // 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; } @@ -806,113 +1020,13 @@ 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; } // Ctrl+U для профиля if key.code == KeyCode::Char('u') && has_ctrl { - if let Some(chat_id) = app.selected_chat_id { - app.status_message = Some("Загрузка профиля...".to_string()); - match with_timeout_msg( - Duration::from_secs(5), - app.td_client.get_profile_info(chat_id), - "Таймаут загрузки профиля", - ) - .await - { - Ok(profile) => { - app.enter_profile_mode(profile); - app.status_message = None; - } - Err(e) => { - app.error_message = Some(e); - app.status_message = None; - } - } - } + handle_profile_open(app).await; return; }