From ac684da8203d9bd0aa24c4cf39dae29a859bdf16 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Tue, 27 Jan 2026 12:31:31 +0300 Subject: [PATCH] add draft messages --- src/app/mod.rs | 20 ++++++++++++ src/input/main_input.rs | 14 +++++++++ src/tdlib/client.rs | 67 +++++++++++++++++++++++++++++++++++++++++ src/ui/chat_list.rs | 9 +++++- src/ui/messages.rs | 15 +++++---- 5 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index d86a479..143c23a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -517,4 +517,24 @@ impl App { pub fn get_selected_search_result_id(&self) -> Option { self.get_selected_search_result().map(|m| m.id) } + + // === Draft Management === + + /// Получить черновик для текущего чата + pub fn get_current_draft(&self) -> Option { + self.selected_chat_id.and_then(|chat_id| { + self.chats + .iter() + .find(|c| c.id == chat_id) + .and_then(|c| c.draft_text.clone()) + }) + } + + /// Загрузить черновик в message_input (вызывается при открытии чата) + pub fn load_draft(&mut self) { + if let Some(draft) = self.get_current_draft() { + self.message_input = draft; + self.cursor_position = self.message_input.chars().count(); + } + } } diff --git a/src/input/main_input.rs b/src/input/main_input.rs index d2c96c6..874a5d8 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -257,6 +257,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) { 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)) => { @@ -386,6 +388,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) { 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)) => { @@ -415,6 +419,16 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Отменить режим ответа 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; diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs index 9880a1a..bd2c40a 100644 --- a/src/tdlib/client.rs +++ b/src/tdlib/client.rs @@ -112,6 +112,8 @@ pub struct ChatInfo { pub folder_ids: Vec, /// Чат замьючен (уведомления отключены) pub is_muted: bool, + /// Черновик сообщения + pub draft_text: Option, } /// Информация о сообщении, на которое отвечают @@ -592,6 +594,19 @@ impl TdClient { } } } + Update::ChatDraftMessage(update) => { + // Обновляем черновик в списке чатов + if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { + chat.draft_text = update.draft_message.as_ref().and_then(|draft| { + // Извлекаем текст из InputMessageText + if let tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) = &draft.input_message_text { + Some(text_msg.text.text.clone()) + } else { + None + } + }); + } + } _ => {} } } @@ -678,6 +693,7 @@ impl TdClient { last_read_outbox_message_id: td_chat.last_read_outbox_message_id, folder_ids, is_muted, + draft_text: None, }; if let Some(existing) = self.chats.iter_mut().find(|c| c.id == td_chat.id) { @@ -1348,6 +1364,57 @@ impl TdClient { } /// Редактирование текстового сообщения с поддержкой Markdown + /// Устанавливает черновик для чата через TDLib API + pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> { + use tdlib_rs::types::{FormattedText, InputMessageText, DraftMessage}; + use tdlib_rs::enums::InputMessageContent; + + if text.is_empty() { + // Очищаем черновик + let result = functions::set_chat_draft_message( + chat_id, + 0, // message_thread_id + None, // draft_message (None = очистить) + self.client_id, + ).await; + + match result { + Ok(_) => Ok(()), + Err(e) => Err(format!("Ошибка очистки черновика: {:?}", e)), + } + } else { + // Создаём черновик + let formatted_text = FormattedText { + text: text.clone(), + entities: vec![], + }; + + let input_message = InputMessageContent::InputMessageText(InputMessageText { + text: formatted_text, + link_preview_options: None, + clear_draft: false, + }); + + let draft = DraftMessage { + reply_to: None, + date: 0, // TDLib установит текущее время + input_message_text: input_message, + }; + + let result = functions::set_chat_draft_message( + chat_id, + 0, // message_thread_id + Some(draft), + self.client_id, + ).await; + + match result { + Ok(_) => Ok(()), + Err(e) => Err(format!("Ошибка установки черновика: {:?}", e)), + } + } + } + pub async fn edit_message(&self, chat_id: i64, message_id: i64, text: String) -> Result { use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown}; use tdlib_rs::enums::{InputMessageContent, TextParseMode}; diff --git a/src/ui/chat_list.rs b/src/ui/chat_list.rs index e060774..4547214 100644 --- a/src/ui/chat_list.rs +++ b/src/ui/chat_list.rs @@ -65,13 +65,20 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { String::new() }; + // Индикатор черновика ✎ + let draft_badge = if chat.draft_text.is_some() { + " ✎".to_string() + } else { + String::new() + }; + let unread_badge = if chat.unread_count > 0 { format!(" ({})", chat.unread_count) } else { String::new() }; - let content = format!("{}{}{}{}{}{}{}{}", prefix, status_icon, pin_icon, mute_icon, chat.title, username_text, mention_badge, unread_badge); + let content = format!("{}{}{}{}{}{}{}{}{}", prefix, status_icon, pin_icon, mute_icon, chat.title, username_text, mention_badge, draft_badge, unread_badge); // Цвет: онлайн — зелёные, остальные — белые let style = match app.td_client.get_user_status_by_chat_id(chat.id) { diff --git a/src/ui/messages.rs b/src/ui/messages.rs index db7d7c9..0516a6a 100644 --- a/src/ui/messages.rs +++ b/src/ui/messages.rs @@ -148,15 +148,18 @@ fn render_input_with_cursor(prefix: &str, text: &str, cursor_pos: usize, color: let chars: Vec = text.chars().collect(); let mut spans: Vec = vec![Span::raw(prefix.to_string())]; + // Ограничиваем cursor_pos границами текста + let safe_cursor_pos = cursor_pos.min(chars.len()); + // Текст до курсора - if cursor_pos > 0 { - let before: String = chars[..cursor_pos].iter().collect(); + if safe_cursor_pos > 0 { + let before: String = chars[..safe_cursor_pos].iter().collect(); spans.push(Span::styled(before, Style::default().fg(color))); } // Символ под курсором (или █ если курсор в конце) - if cursor_pos < chars.len() { - let cursor_char = chars[cursor_pos].to_string(); + if safe_cursor_pos < chars.len() { + let cursor_char = chars[safe_cursor_pos].to_string(); spans.push(Span::styled(cursor_char, Style::default().fg(Color::Black).bg(color))); } else { // Курсор в конце - показываем блок @@ -164,8 +167,8 @@ fn render_input_with_cursor(prefix: &str, text: &str, cursor_pos: usize, color: } // Текст после курсора - if cursor_pos + 1 < chars.len() { - let after: String = chars[cursor_pos + 1..].iter().collect(); + if safe_cursor_pos + 1 < chars.len() { + let after: String = chars[safe_cursor_pos + 1..].iter().collect(); spans.push(Span::styled(after, Style::default().fg(color))); }