1112 lines
49 KiB
Rust
1112 lines
49 KiB
Rust
use crate::app::App;
|
||
use crate::tdlib::ChatAction;
|
||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||
use std::time::{Duration, Instant};
|
||
use tokio::time::timeout;
|
||
|
||
pub async fn handle(app: &mut App, key: KeyEvent) {
|
||
let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||
|
||
// Глобальные команды (работают всегда)
|
||
match key.code {
|
||
KeyCode::Char('r') if has_ctrl => {
|
||
app.status_message = Some("Обновление чатов...".to_string());
|
||
let _ = timeout(Duration::from_secs(5), app.td_client.load_chats(50)).await;
|
||
app.status_message = None;
|
||
return;
|
||
}
|
||
KeyCode::Char('s') if has_ctrl => {
|
||
// Ctrl+S - начать поиск (только если чат не открыт)
|
||
if app.selected_chat_id.is_none() {
|
||
app.start_search();
|
||
}
|
||
return;
|
||
}
|
||
KeyCode::Char('p') if has_ctrl => {
|
||
// Ctrl+P - режим просмотра закреплённых сообщений
|
||
if app.selected_chat_id.is_some() && !app.is_pinned_mode() {
|
||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||
app.status_message = Some("Загрузка закреплённых...".to_string());
|
||
match timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client.get_pinned_messages(chat_id),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Ok(messages)) => {
|
||
if messages.is_empty() {
|
||
app.status_message = Some("Нет закреплённых сообщений".to_string());
|
||
} else {
|
||
app.enter_pinned_mode(messages);
|
||
app.status_message = None;
|
||
}
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(e);
|
||
app.status_message = None;
|
||
}
|
||
Err(_) => {
|
||
app.error_message = Some("Таймаут загрузки".to_string());
|
||
app.status_message = None;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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_profile_mode() {
|
||
// Обработка подтверждения выхода из группы
|
||
let confirmation_step = app.get_leave_group_confirmation_step();
|
||
if confirmation_step > 0 {
|
||
match key.code {
|
||
KeyCode::Char('y') | KeyCode::Char('н') | KeyCode::Enter => {
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
KeyCode::Char('n') | KeyCode::Char('т') | KeyCode::Esc => {
|
||
// Отмена
|
||
app.cancel_leave_group();
|
||
}
|
||
_ => {}
|
||
}
|
||
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('@')
|
||
);
|
||
match open::that(&url) {
|
||
Ok(_) => {
|
||
app.status_message = Some(format!("Открыто: {}", url));
|
||
}
|
||
Err(e) => {
|
||
app.error_message =
|
||
Some(format!("Ошибка открытия браузера: {}", e));
|
||
}
|
||
}
|
||
}
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
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 => {
|
||
// Удаляем символ из запроса
|
||
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(Ok(results)) = timeout(
|
||
Duration::from_secs(3),
|
||
app.td_client.search_messages(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(Ok(results)) = timeout(
|
||
Duration::from_secs(3),
|
||
app.td_client.search_messages(chat_id, &query),
|
||
)
|
||
.await
|
||
{
|
||
app.set_search_results(results);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
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_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();
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
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 {
|
||
app.status_message = Some("Отправка реакции...".to_string());
|
||
app.needs_redraw = true;
|
||
|
||
match timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client
|
||
.toggle_reaction(chat_id, message_id, emoji.clone()),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Ok(_)) => {
|
||
app.status_message =
|
||
Some(format!("Реакция {} добавлена", emoji));
|
||
app.exit_reaction_picker_mode();
|
||
app.needs_redraw = true;
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(format!("Ошибка: {}", e));
|
||
app.status_message = None;
|
||
app.needs_redraw = true;
|
||
}
|
||
Err(_) => {
|
||
app.error_message =
|
||
Some("Таймаут отправки реакции".to_string());
|
||
app.status_message = None;
|
||
app.needs_redraw = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
KeyCode::Esc => {
|
||
app.exit_reaction_picker_mode();
|
||
app.needs_redraw = true;
|
||
}
|
||
_ => {}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Модалка подтверждения удаления
|
||
if app.is_confirm_delete_shown() {
|
||
match key.code {
|
||
KeyCode::Char('y') | KeyCode::Char('н') | KeyCode::Enter => {
|
||
// Подтверждение удаления
|
||
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 timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client.delete_messages(
|
||
chat_id,
|
||
vec![msg_id],
|
||
can_delete_for_all,
|
||
),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Ok(_)) => {
|
||
// Удаляем из локального списка
|
||
app.td_client
|
||
.current_chat_messages_mut()
|
||
.retain(|m| m.id != msg_id);
|
||
// Сбрасываем состояние
|
||
app.chat_state = crate::app::ChatState::Normal;
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(e);
|
||
}
|
||
Err(_) => {
|
||
app.error_message = Some("Таймаут удаления".to_string());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Закрываем модалку
|
||
app.chat_state = crate::app::ChatState::Normal;
|
||
}
|
||
KeyCode::Char('n') | KeyCode::Char('т') | KeyCode::Esc => {
|
||
// Отмена удаления
|
||
app.chat_state = crate::app::ChatState::Normal;
|
||
}
|
||
_ => {}
|
||
}
|
||
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 timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client.forward_messages(
|
||
to_chat_id,
|
||
from_chat_id,
|
||
vec![msg_id],
|
||
),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Ok(_)) => {
|
||
app.status_message =
|
||
Some("Сообщение переслано".to_string());
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(e);
|
||
}
|
||
Err(_) => {
|
||
app.error_message = Some("Таймаут пересылки".to_string());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
app.cancel_forward();
|
||
}
|
||
KeyCode::Down => {
|
||
app.next_chat();
|
||
}
|
||
KeyCode::Up => {
|
||
app.previous_chat();
|
||
}
|
||
_ => {}
|
||
}
|
||
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() {
|
||
app.status_message = Some("Загрузка сообщений...".to_string());
|
||
app.message_scroll_offset = 0;
|
||
match timeout(
|
||
Duration::from_secs(10),
|
||
app.td_client.get_chat_history(chat_id, 100),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Ok(_)) => {
|
||
// Загружаем недостающие reply info
|
||
let _ = timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client.fetch_missing_reply_info(),
|
||
)
|
||
.await;
|
||
// Загружаем последнее закреплённое сообщение
|
||
let _ = timeout(
|
||
Duration::from_secs(2),
|
||
app.td_client.load_current_pinned_message(chat_id),
|
||
)
|
||
.await;
|
||
// Загружаем черновик
|
||
app.load_draft();
|
||
app.status_message = None;
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(e);
|
||
app.status_message = None;
|
||
}
|
||
Err(_) => {
|
||
app.error_message = Some("Таймаут загрузки сообщений".to_string());
|
||
app.status_message = None;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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));
|
||
}
|
||
_ => {}
|
||
}
|
||
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 !app.message_input.is_empty() {
|
||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||
let text = app.message_input.clone();
|
||
|
||
if let Some(msg_id) = app.chat_state.selected_message_id() {
|
||
if app.is_editing() {
|
||
// Режим редактирования
|
||
app.message_input.clear();
|
||
app.cursor_position = 0;
|
||
app.chat_state = crate::app::ChatState::Normal;
|
||
|
||
match timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client.edit_message(chat_id, msg_id, text),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Ok(edited_msg)) => {
|
||
// Обновляем сообщение в списке
|
||
if let Some(msg) = app
|
||
.td_client
|
||
.current_chat_messages_mut()
|
||
.iter_mut()
|
||
.find(|m| m.id == msg_id)
|
||
{
|
||
msg.content = edited_msg.content;
|
||
msg.entities = edited_msg.entities;
|
||
msg.edit_date = edited_msg.edit_date;
|
||
}
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(e);
|
||
}
|
||
Err(_) => {
|
||
app.error_message = Some("Таймаут редактирования".to_string());
|
||
}
|
||
}
|
||
}
|
||
} 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.clone(),
|
||
text: m.content.clone(),
|
||
}
|
||
});
|
||
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(chat_id, ChatAction::Cancel)
|
||
.await;
|
||
|
||
match timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client
|
||
.send_message(chat_id, text, reply_to_id, reply_info),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Ok(sent_msg)) => {
|
||
// Добавляем отправленное сообщение в список (с лимитом)
|
||
app.td_client.push_message(sent_msg);
|
||
// Сбрасываем скролл чтобы видеть новое сообщение
|
||
app.message_scroll_offset = 0;
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(e);
|
||
}
|
||
Err(_) => {
|
||
app.error_message = Some("Таймаут отправки".to_string());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} 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() {
|
||
app.status_message = Some("Загрузка сообщений...".to_string());
|
||
app.message_scroll_offset = 0;
|
||
match timeout(
|
||
Duration::from_secs(10),
|
||
app.td_client.get_chat_history(chat_id, 100),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Ok(_)) => {
|
||
// Загружаем недостающие reply info
|
||
let _ = timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client.fetch_missing_reply_info(),
|
||
)
|
||
.await;
|
||
// Загружаем последнее закреплённое сообщение
|
||
let _ = timeout(
|
||
Duration::from_secs(2),
|
||
app.td_client.load_current_pinned_message(chat_id),
|
||
)
|
||
.await;
|
||
// Загружаем черновик
|
||
app.load_draft();
|
||
app.status_message = None;
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(e);
|
||
app.status_message = None;
|
||
}
|
||
Err(_) => {
|
||
app.error_message = Some("Таймаут загрузки сообщений".to_string());
|
||
app.status_message = None;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Режим открытого чата
|
||
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 timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client
|
||
.get_message_available_reactions(chat_id, message_id),
|
||
)
|
||
.await
|
||
{
|
||
Ok(Ok(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, reactions);
|
||
app.status_message = None;
|
||
app.needs_redraw = true;
|
||
}
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(format!("Ошибка загрузки реакций: {}", e));
|
||
app.status_message = None;
|
||
app.needs_redraw = true;
|
||
}
|
||
Err(_) => {
|
||
app.error_message = Some("Таймаут загрузки реакций".to_string());
|
||
app.status_message = None;
|
||
app.needs_redraw = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
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 timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await
|
||
{
|
||
Ok(Ok(profile)) => {
|
||
app.enter_profile_mode(profile);
|
||
app.status_message = None;
|
||
}
|
||
Ok(Err(e)) => {
|
||
app.error_message = Some(e);
|
||
app.status_message = None;
|
||
}
|
||
Err(_) => {
|
||
app.error_message = Some("Таймаут загрузки профиля".to_string());
|
||
app.status_message = None;
|
||
}
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
match key.code {
|
||
KeyCode::Backspace => {
|
||
// Удаляем символ слева от курсора
|
||
if app.cursor_position > 0 {
|
||
let chars: Vec<char> = app.message_input.chars().collect();
|
||
let mut new_input = String::new();
|
||
for (i, ch) in chars.iter().enumerate() {
|
||
if i != app.cursor_position - 1 {
|
||
new_input.push(*ch);
|
||
}
|
||
}
|
||
app.message_input = new_input;
|
||
app.cursor_position -= 1;
|
||
}
|
||
}
|
||
KeyCode::Delete => {
|
||
// Удаляем символ справа от курсора
|
||
let len = app.message_input.chars().count();
|
||
if app.cursor_position < len {
|
||
let chars: Vec<char> = app.message_input.chars().collect();
|
||
let mut new_input = String::new();
|
||
for (i, ch) in chars.iter().enumerate() {
|
||
if i != app.cursor_position {
|
||
new_input.push(*ch);
|
||
}
|
||
}
|
||
app.message_input = new_input;
|
||
}
|
||
}
|
||
KeyCode::Char(c) => {
|
||
// Вставляем символ в позицию курсора
|
||
let chars: Vec<char> = app.message_input.chars().collect();
|
||
let mut new_input = String::new();
|
||
for (i, ch) in chars.iter().enumerate() {
|
||
if i == app.cursor_position {
|
||
new_input.push(c);
|
||
}
|
||
new_input.push(*ch);
|
||
}
|
||
if app.cursor_position >= chars.len() {
|
||
new_input.push(c);
|
||
}
|
||
app.message_input = new_input;
|
||
app.cursor_position += 1;
|
||
|
||
// Отправляем typing status с throttling (не чаще 1 раза в 5 сек)
|
||
let should_send_typing = app
|
||
.last_typing_sent
|
||
.map(|t| t.elapsed().as_secs() >= 5)
|
||
.unwrap_or(true);
|
||
if should_send_typing {
|
||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||
app.td_client
|
||
.send_chat_action(chat_id, ChatAction::Typing)
|
||
.await;
|
||
app.last_typing_sent = Some(Instant::now());
|
||
}
|
||
}
|
||
}
|
||
KeyCode::Left => {
|
||
// Курсор влево
|
||
if app.cursor_position > 0 {
|
||
app.cursor_position -= 1;
|
||
}
|
||
}
|
||
KeyCode::Right => {
|
||
// Курсор вправо
|
||
let len = app.message_input.chars().count();
|
||
if app.cursor_position < len {
|
||
app.cursor_position += 1;
|
||
}
|
||
}
|
||
KeyCode::Home => {
|
||
// Курсор в начало
|
||
app.cursor_position = 0;
|
||
}
|
||
KeyCode::End => {
|
||
// Курсор в конец
|
||
app.cursor_position = app.message_input.chars().count();
|
||
}
|
||
// Стрелки вверх/вниз - скролл сообщений или начало выбора
|
||
KeyCode::Down => {
|
||
// Скролл вниз (к новым сообщениям)
|
||
if app.message_scroll_offset > 0 {
|
||
app.message_scroll_offset = app.message_scroll_offset.saturating_sub(3);
|
||
}
|
||
}
|
||
KeyCode::Up => {
|
||
// Если инпут пустой и не в режиме редактирования — начать выбор сообщения
|
||
if app.message_input.is_empty() && !app.is_editing() {
|
||
app.start_message_selection();
|
||
} else {
|
||
// Скролл вверх (к старым сообщениям)
|
||
app.message_scroll_offset += 3;
|
||
|
||
// Проверяем, нужно ли подгрузить старые сообщения
|
||
if !app.td_client.current_chat_messages().is_empty() {
|
||
let oldest_msg_id = app
|
||
.td_client
|
||
.current_chat_messages()
|
||
.first()
|
||
.map(|m| m.id)
|
||
.unwrap_or(0);
|
||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||
// Подгружаем больше сообщений если скролл близко к верху
|
||
if app.message_scroll_offset
|
||
> app.td_client.current_chat_messages().len().saturating_sub(10)
|
||
{
|
||
if let Ok(Ok(older)) = timeout(
|
||
Duration::from_secs(3),
|
||
app.td_client
|
||
.load_older_messages(chat_id, oldest_msg_id),
|
||
)
|
||
.await
|
||
{
|
||
if !older.is_empty() {
|
||
// Добавляем старые сообщения в начало
|
||
let msgs = app.td_client.current_chat_messages_mut();
|
||
msgs.splice(0..0, older);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
} else {
|
||
// В режиме списка чатов - навигация стрелками и переключение папок
|
||
match key.code {
|
||
KeyCode::Down => {
|
||
app.next_chat();
|
||
}
|
||
KeyCode::Up => {
|
||
app.previous_chat();
|
||
}
|
||
// Цифры 1-9 - переключение папок
|
||
KeyCode::Char(c) if c >= '1' && c <= '9' => {
|
||
let folder_num = (c as usize) - ('1' as usize); // 0-based
|
||
if folder_num == 0 {
|
||
// 1 = All
|
||
app.selected_folder_id = None;
|
||
} else {
|
||
// 2, 3, 4... = папки из TDLib
|
||
if let Some(folder) = app.td_client.folders().get(folder_num - 1) {
|
||
let folder_id = folder.id;
|
||
app.selected_folder_id = Some(folder_id);
|
||
// Загружаем чаты папки
|
||
app.status_message = Some("Загрузка чатов папки...".to_string());
|
||
let _ = timeout(
|
||
Duration::from_secs(5),
|
||
app.td_client.load_folder_chats(folder_id, 50),
|
||
)
|
||
.await;
|
||
app.status_message = None;
|
||
}
|
||
}
|
||
app.chat_list_state.select(Some(0));
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Подсчёт количества доступных действий в профиле
|
||
fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize {
|
||
let mut count = 0;
|
||
|
||
if profile.username.is_some() {
|
||
count += 1; // Открыть в браузере
|
||
}
|
||
|
||
count += 1; // Скопировать ID
|
||
|
||
if profile.is_group {
|
||
count += 1; // Покинуть группу
|
||
}
|
||
|
||
count
|
||
}
|
||
|
||
/// Копирует текст в системный буфер обмена
|
||
fn copy_to_clipboard(text: &str) -> Result<(), String> {
|
||
use arboard::Clipboard;
|
||
|
||
let mut clipboard =
|
||
Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?;
|
||
clipboard
|
||
.set_text(text)
|
||
.map_err(|e| format!("Не удалось скопировать: {}", e))?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Форматирует сообщение для копирования с контекстом
|
||
fn format_message_for_clipboard(msg: &crate::tdlib::MessageInfo) -> String {
|
||
let mut result = String::new();
|
||
|
||
// Добавляем forward контекст если есть
|
||
if let Some(forward) = &msg.forward_from {
|
||
result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name));
|
||
}
|
||
|
||
// Добавляем reply контекст если есть
|
||
if let Some(reply) = &msg.reply_to {
|
||
result.push_str(&format!("┌ {}: {}\n", reply.sender_name, reply.text));
|
||
}
|
||
|
||
// Добавляем основной текст с markdown форматированием
|
||
result.push_str(&convert_entities_to_markdown(&msg.content, &msg.entities));
|
||
|
||
result
|
||
}
|
||
|
||
/// Конвертирует текст с entities в markdown
|
||
fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEntity]) -> String {
|
||
use tdlib_rs::enums::TextEntityType;
|
||
|
||
if entities.is_empty() {
|
||
return text.to_string();
|
||
}
|
||
|
||
// Создаём вектор символов для работы с unicode
|
||
let chars: Vec<char> = text.chars().collect();
|
||
let mut result = String::new();
|
||
let mut i = 0;
|
||
|
||
while i < chars.len() {
|
||
// Ищем entity, который начинается в текущей позиции
|
||
let mut entity_found = false;
|
||
|
||
for entity in entities {
|
||
if entity.offset as usize == i {
|
||
entity_found = true;
|
||
let end = (entity.offset + entity.length) as usize;
|
||
let entity_text: String = chars[i..end.min(chars.len())].iter().collect();
|
||
|
||
// Применяем форматирование в зависимости от типа
|
||
let formatted = match &entity.r#type {
|
||
TextEntityType::Bold => format!("**{}**", entity_text),
|
||
TextEntityType::Italic => format!("*{}*", entity_text),
|
||
TextEntityType::Underline => format!("__{}__", entity_text),
|
||
TextEntityType::Strikethrough => format!("~~{}~~", entity_text),
|
||
TextEntityType::Code | TextEntityType::Pre | TextEntityType::PreCode(_) => {
|
||
format!("`{}`", entity_text)
|
||
}
|
||
TextEntityType::TextUrl(url_info) => {
|
||
format!("[{}]({})", entity_text, url_info.url)
|
||
}
|
||
TextEntityType::Url => format!("<{}>", entity_text),
|
||
TextEntityType::Mention | TextEntityType::MentionName(_) => {
|
||
format!("@{}", entity_text.trim_start_matches('@'))
|
||
}
|
||
TextEntityType::Spoiler => format!("||{}||", entity_text),
|
||
_ => entity_text,
|
||
};
|
||
|
||
result.push_str(&formatted);
|
||
i = end;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if !entity_found {
|
||
result.push(chars[i]);
|
||
i += 1;
|
||
}
|
||
}
|
||
|
||
result
|
||
}
|