This commit is contained in:
Mikhail Kilin
2026-01-30 15:07:13 +03:00
parent 126c7482af
commit 4deb0fbe00
32 changed files with 1049 additions and 697 deletions

View File

@@ -1,8 +1,8 @@
use crate::app::App;
use crate::tdlib::client::AuthState;
use crossterm::event::KeyCode;
use std::time::Duration;
use tokio::time::timeout;
use crate::app::App;
use crate::tdlib::client::AuthState;
pub async fn handle(app: &mut App, key_code: KeyCode) {
match &app.td_client.auth_state {
@@ -18,7 +18,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) {
KeyCode::Enter => {
if !app.phone_input.is_empty() {
app.status_message = Some("Отправка номера...".to_string());
match timeout(Duration::from_secs(10), app.td_client.send_phone_number(app.phone_input.clone())).await {
match timeout(
Duration::from_secs(10),
app.td_client.send_phone_number(app.phone_input.clone()),
)
.await
{
Ok(Ok(_)) => {
app.error_message = None;
app.status_message = None;
@@ -48,7 +53,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) {
KeyCode::Enter => {
if !app.code_input.is_empty() {
app.status_message = Some("Проверка кода...".to_string());
match timeout(Duration::from_secs(10), app.td_client.send_code(app.code_input.clone())).await {
match timeout(
Duration::from_secs(10),
app.td_client.send_code(app.code_input.clone()),
)
.await
{
Ok(Ok(_)) => {
app.error_message = None;
app.status_message = None;
@@ -78,7 +88,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) {
KeyCode::Enter => {
if !app.password_input.is_empty() {
app.status_message = Some("Проверка пароля...".to_string());
match timeout(Duration::from_secs(10), app.td_client.send_password(app.password_input.clone())).await {
match timeout(
Duration::from_secs(10),
app.td_client.send_password(app.password_input.clone()),
)
.await
{
Ok(Ok(_)) => {
app.error_message = None;
app.status_message = None;

View File

@@ -1,8 +1,8 @@
use crate::app::App;
use crate::tdlib::ChatAction;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::time::{Duration, Instant};
use tokio::time::timeout;
use crate::app::App;
use crate::tdlib::ChatAction;
pub async fn handle(app: &mut App, key: KeyEvent) {
let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
@@ -27,7 +27,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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 {
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());
@@ -51,7 +56,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
}
KeyCode::Char('f') if has_ctrl => {
// Ctrl+F - поиск по сообщениям в открытом чате
if app.selected_chat_id.is_some() && !app.is_pinned_mode() && !app.is_message_search_mode() {
if app.selected_chat_id.is_some()
&& !app.is_pinned_mode()
&& !app.is_message_search_mode()
{
app.enter_message_search_mode();
}
return;
@@ -125,13 +133,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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('@'));
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));
app.error_message =
Some(format!("Ошибка открытия браузера: {}", e));
}
}
}
@@ -142,7 +154,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
// Действие: Скопировать ID
if action_index == current_idx {
app.status_message = Some(format!("ID скопирован: {}", profile.chat_id));
app.status_message =
Some(format!("ID скопирован: {}", profile.chat_id));
return;
}
current_idx += 1;
@@ -174,10 +187,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
KeyCode::Enter => {
// Перейти к выбранному сообщению
if let Some(msg_id) = app.get_selected_search_result_id() {
let msg_index = app.td_client.current_chat_messages
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);
@@ -192,8 +207,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if !app.message_search_query.is_empty() {
if let Ok(Ok(results)) = timeout(
Duration::from_secs(3),
app.td_client.search_messages(chat_id, &app.message_search_query)
).await {
app.td_client
.search_messages(chat_id, &app.message_search_query),
)
.await
{
app.set_search_results(results);
}
} else {
@@ -207,8 +225,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(chat_id) = app.get_selected_chat_id() {
if let Ok(Ok(results)) = timeout(
Duration::from_secs(3),
app.td_client.search_messages(chat_id, &app.message_search_query)
).await {
app.td_client
.search_messages(chat_id, &app.message_search_query),
)
.await
{
app.set_search_results(results);
}
}
@@ -234,10 +255,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
// Перейти к сообщению в истории
if let Some(msg_id) = app.get_selected_pinned_id() {
// Ищем индекс сообщения в текущей истории
let msg_index = app.td_client.current_chat_messages
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();
@@ -284,13 +307,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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 {
app.td_client
.toggle_reaction(chat_id, message_id, emoji.clone()),
)
.await
{
Ok(Ok(_)) => {
app.status_message = Some(format!("Реакция {} добавлена", emoji));
app.status_message =
Some(format!("Реакция {} добавлена", emoji));
app.exit_reaction_picker_mode();
app.needs_redraw = true;
}
@@ -300,7 +327,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
app.needs_redraw = true;
}
Err(_) => {
app.error_message = Some("Таймаут отправки реакции".to_string());
app.error_message =
Some("Таймаут отправки реакции".to_string());
app.status_message = None;
app.needs_redraw = true;
}
@@ -326,7 +354,9 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(msg_id) = app.confirm_delete_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
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)
@@ -334,11 +364,19 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
match timeout(
Duration::from_secs(5),
app.td_client.delete_messages(chat_id, vec![msg_id], can_delete_for_all)
).await {
app.td_client.delete_messages(
chat_id,
vec![msg_id],
can_delete_for_all,
),
)
.await
{
Ok(Ok(_)) => {
// Удаляем из локального списка
app.td_client.current_chat_messages.retain(|m| m.id != msg_id);
app.td_client
.current_chat_messages
.retain(|m| m.id != msg_id);
app.selected_message_index = None;
}
Ok(Err(e)) => {
@@ -377,10 +415,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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 {
app.td_client.forward_messages(
to_chat_id,
from_chat_id,
vec![msg_id],
),
)
.await
{
Ok(Ok(_)) => {
app.status_message = Some("Сообщение переслано".to_string());
app.status_message =
Some("Сообщение переслано".to_string());
}
Ok(Err(e)) => {
app.error_message = Some(e);
@@ -418,12 +463,25 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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 {
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(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;
let _ = timeout(
Duration::from_secs(2),
app.td_client.load_current_pinned_message(chat_id),
)
.await;
// Загружаем черновик
app.load_draft();
app.status_message = None;
@@ -460,8 +518,6 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
return;
}
// Enter - открыть чат, отправить сообщение или редактировать
if key.code == KeyCode::Enter {
if app.selected_chat_id.is_some() {
@@ -488,10 +544,20 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
app.cursor_position = 0;
app.editing_message_id = None;
match timeout(Duration::from_secs(5), app.td_client.edit_message(chat_id, msg_id, text)).await {
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.iter_mut().find(|m| m.id == msg_id) {
if let Some(msg) = app
.td_client
.current_chat_messages
.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;
@@ -521,9 +587,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
app.last_typing_sent = None;
// Отменяем typing status
app.td_client.send_chat_action(chat_id, ChatAction::Cancel).await;
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 {
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);
@@ -549,12 +623,25 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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 {
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(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;
let _ = timeout(
Duration::from_secs(2),
app.td_client.load_current_pinned_message(chat_id),
)
.await;
// Загружаем черновик
app.load_draft();
app.status_message = None;
@@ -593,7 +680,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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;
let _ = app
.td_client
.set_draft_message(chat_id, String::new())
.await;
}
}
app.close_chat();
@@ -616,7 +706,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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;
let can_delete =
msg.can_be_deleted_only_for_self || msg.can_be_deleted_for_all_users;
if can_delete {
app.confirm_delete_message_id = Some(msg.id);
}
@@ -649,18 +740,22 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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 {
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.error_message =
Some("Реакции недоступны для этого сообщения".to_string());
app.status_message = None;
app.needs_redraw = true;
} else {
@@ -691,7 +786,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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 {
match timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await
{
Ok(Ok(profile)) => {
app.profile_info = Some(profile);
app.enter_profile_mode();
@@ -756,12 +852,15 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
app.cursor_position += 1;
// Отправляем typing status с throttling (не чаще 1 раза в 5 сек)
let should_send_typing = app.last_typing_sent
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.td_client
.send_chat_action(chat_id, ChatAction::Typing)
.await;
app.last_typing_sent = Some(Instant::now());
}
}
@@ -804,18 +903,29 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
// Проверяем, нужно ли подгрузить старые сообщения
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);
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 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, 20)
).await {
app.td_client
.load_older_messages(chat_id, oldest_msg_id, 20),
)
.await
{
if !older.is_empty() {
// Добавляем старые сообщения в начало
let mut new_messages = older;
new_messages.extend(app.td_client.current_chat_messages.drain(..));
new_messages
.extend(app.td_client.current_chat_messages.drain(..));
app.td_client.current_chat_messages = new_messages;
}
}
@@ -848,7 +958,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
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;
let _ = timeout(
Duration::from_secs(5),
app.td_client.load_folder_chats(folder_id, 50),
)
.await;
app.status_message = None;
}
}
@@ -862,73 +976,76 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
/// Подсчёт количества доступных действий в профиле
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))?;
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::client::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),
@@ -948,18 +1065,18 @@ fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEnt
TextEntityType::Spoiler => format!("||{}||", entity_text),
_ => entity_text,
};
result.push_str(&formatted);
i = end;
break;
}
}
if !entity_found {
result.push(chars[i]);
i += 1;
}
}
result
}