Files
telegram-tui/src/input/main_input.rs
Mikhail Kilin bba5cbd22d fixes
2026-01-30 23:55:01 +03:00

1112 lines
49 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}