From 43960332d9862417a621b1bc677b2b7bf621ac98 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Sat, 31 Jan 2026 01:45:54 +0300 Subject: [PATCH] refactor: restructure MessageInfo with logical field grouping (P2.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сгруппированы 16 плоских полей MessageInfo в 4 логические структуры для улучшения организации кода и maintainability. Новые структуры: - MessageMetadata: id, sender_name, date, edit_date - MessageContent: text, entities - MessageState: is_outgoing, is_read, can_be_edited, can_be_deleted_* - MessageInteractions: reply_to, forward_from, reactions Изменения: - Добавлены 4 новые структуры в tdlib/types.rs - Обновлена MessageInfo для использования новых структур - Добавлен конструктор MessageInfo::new() для удобного создания - Добавлены getter методы (id(), text(), sender_name() и др.) для удобного доступа - Обновлены все места создания MessageInfo (convert_message) - Обновлены все места использования (~200+ обращений): * ui/messages.rs: рендеринг сообщений * app/mod.rs: логика приложения * input/main_input.rs: обработка ввода и копирование * tdlib/client.rs: обработка updates * Все тестовые файлы (14 файлов) Преимущества: - Логическая группировка данных - Проще понимать структуру сообщения - Легче добавлять новые поля в будущем - Улучшенная читаемость кода Статус: Priority 2 теперь 80% (4/5 задач) - ✅ Error enum - ✅ Config validation - ✅ Newtype для ID - ✅ MessageInfo реструктуризация - ⏳ MessageBuilder pattern Co-Authored-By: Claude Sonnet 4.5 --- src/app/mod.rs | 16 ++-- src/input/main_input.rs | 34 ++++---- src/tdlib/client.rs | 38 ++++----- src/tdlib/types.rs | 140 +++++++++++++++++++++++++++++++-- src/ui/messages.rs | 88 ++++++++++----------- tests/copy.rs | 2 +- tests/delete_message.rs | 4 +- tests/edit_message.rs | 10 +-- tests/helpers/fake_tdclient.rs | 6 +- tests/helpers/test_data.rs | 32 ++++---- tests/navigation.rs | 4 +- tests/reply_forward.rs | 4 +- tests/search.rs | 20 ++--- tests/send_message.rs | 20 ++--- 14 files changed, 274 insertions(+), 144 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 9fc6466..39bbdd0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -188,8 +188,8 @@ impl App { // Сначала извлекаем данные из сообщения let msg_data = self.get_selected_message().and_then(|msg| { - if msg.can_be_edited && msg.is_outgoing { - Some((msg.id, msg.content.clone(), selected_idx.unwrap())) + if msg.can_be_edited() && msg.is_outgoing() { + Some((msg.id()(), msg.text().to_string(), selected_idx.unwrap())) } else { None } @@ -328,7 +328,7 @@ impl App { pub fn start_reply_to_selected(&mut self) -> bool { if let Some(msg) = self.get_selected_message() { self.chat_state = ChatState::Reply { - message_id: msg.id, + message_id: msg.id(), }; return true; } @@ -351,7 +351,7 @@ impl App { self.td_client .current_chat_messages() .iter() - .find(|m| m.id == id) + .find(|m| m.id() == id) }) } @@ -359,7 +359,7 @@ impl App { pub fn start_forward_selected(&mut self) -> bool { if let Some(msg) = self.get_selected_message() { self.chat_state = ChatState::Forward { - message_id: msg.id, + message_id: msg.id(), selecting_chat: true, }; // Сбрасываем выбор чата на первый @@ -388,7 +388,7 @@ impl App { self.td_client .current_chat_messages() .iter() - .find(|m| m.id == id) + .find(|m| m.id() == id) }) } @@ -451,7 +451,7 @@ impl App { /// Получить ID текущего pinned для перехода в историю pub fn get_selected_pinned_id(&self) -> Option { - self.get_selected_pinned().map(|m| m.id) + self.get_selected_pinned().map(|m| m.id()) } // === Message Search Mode === @@ -522,7 +522,7 @@ impl App { /// Получить ID выбранного результата для перехода pub fn get_selected_search_result_id(&self) -> Option { - self.get_selected_search_result().map(|m| m.id) + self.get_selected_search_result().map(|m| m.id()) } /// Получить поисковый запрос из режима поиска diff --git a/src/input/main_input.rs b/src/input/main_input.rs index 50a1601..8e72525 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -193,7 +193,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { .td_client .current_chat_messages() .iter() - .position(|m| m.id == msg_id); + .position(|m| m.id() == msg_id); if let Some(idx) = msg_index { let total = app.td_client.current_chat_messages().len(); @@ -268,7 +268,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { .td_client .current_chat_messages() .iter() - .position(|m| m.id == msg_id); + .position(|m| m.id() == msg_id); if let Some(idx) = msg_index { // Вычисляем scroll offset чтобы показать сообщение @@ -381,8 +381,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) { .td_client .current_chat_messages() .iter() - .find(|m| m.id == msg_id) - .map(|m| m.can_be_deleted_for_all_users) + .find(|m| m.id() == msg_id) + .map(|m| m.can_be_deleted_for_all_users()) .unwrap_or(false); match timeout( @@ -399,7 +399,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Удаляем из локального списка app.td_client .current_chat_messages_mut() - .retain(|m| m.id != msg_id); + .retain(|m| m.id() != msg_id); // Сбрасываем состояние app.chat_state = crate::app::ChatState::Normal; } @@ -582,11 +582,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) { .td_client .current_chat_messages_mut() .iter_mut() - .find(|m| m.id == msg_id) + .find(|m| m.id() == msg_id) { - msg.content = edited_msg.content; - msg.entities = edited_msg.entities; - msg.edit_date = edited_msg.edit_date; + msg.content.text = edited_msg.content.text; + msg.content.entities = edited_msg.content.entities; + msg.metadata.edit_date = edited_msg.metadata.edit_date; } } Ok(Err(e)) => { @@ -607,9 +607,9 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Создаём 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(), + message_id: m.id(), + sender_name: m.sender_name().to_string(), + text: m.text().to_string(), } }); app.message_input.clear(); @@ -741,10 +741,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Показать модалку подтверждения удаления 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; + 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, + message_id: msg.id(), }; } } @@ -775,7 +775,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Открыть emoji picker для добавления реакции if let Some(msg) = app.get_selected_message() { let chat_id = app.selected_chat_id.unwrap(); - let message_id = msg.id; + let message_id = msg.id(); app.status_message = Some("Загрузка реакций...".to_string()); app.needs_redraw = true; @@ -942,7 +942,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { .td_client .current_chat_messages() .first() - .map(|m| m.id) + .map(|m| m.id()) .unwrap_or(0); if let Some(chat_id) = app.get_selected_chat_id() { // Подгружаем больше сообщений если скролл близко к верху @@ -1051,7 +1051,7 @@ fn format_message_for_clipboard(msg: &crate::tdlib::MessageInfo) -> String { } // Добавляем основной текст с markdown форматированием - result.push_str(&convert_entities_to_markdown(&msg.content, &msg.entities)); + result.push_str(&convert_entities_to_markdown(msg.text(), msg.entities())); result } diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs index 4713c41..36a0014 100644 --- a/src/tdlib/client.rs +++ b/src/tdlib/client.rs @@ -422,8 +422,8 @@ impl TdClient { // Если это текущий открытый чат — обновляем is_read у сообщений if Some(ChatId::new(update.chat_id)) == self.current_chat_id() { for msg in self.current_chat_messages_mut().iter_mut() { - if msg.is_outgoing && msg.id <= last_read_msg_id { - msg.is_read = true; + if msg.is_outgoing() && msg.id() <= last_read_msg_id { + msg.state.is_read = true; } } } @@ -477,7 +477,7 @@ impl TdClient { let existing_idx = self .current_chat_messages() .iter() - .position(|m| m.id == msg_info.id); + .position(|m| m.id() == msg_info.id()); match existing_idx { Some(idx) => { @@ -646,10 +646,10 @@ impl TdClient { if let Some(msg) = self .current_chat_messages_mut() .iter_mut() - .find(|m| m.id == MessageId::new(update.message_id)) + .find(|m| m.id() == MessageId::new(update.message_id)) { // Извлекаем реакции из interaction_info - msg.reactions = update + msg.interactions.reactions = update .interaction_info .as_ref() .and_then(|info| info.reactions.as_ref()) @@ -870,22 +870,22 @@ impl TdClient { // Извлекаем реакции let reactions = self.extract_reactions(message); - MessageInfo { - id: message_id, + MessageInfo::new( + message_id, sender_name, - is_outgoing: message.is_outgoing, + message.is_outgoing, content, entities, - date: message.date, - edit_date: message.edit_date, + message.date, + message.edit_date, is_read, - can_be_edited: message.can_be_edited, - can_be_deleted_only_for_self: message.can_be_deleted_only_for_self, - can_be_deleted_for_all_users: message.can_be_deleted_for_all_users, + message.can_be_edited, + message.can_be_deleted_only_for_self, + message.can_be_deleted_for_all_users, reply_to, forward_from, reactions, - } + ) } /// Извлекает информацию о reply из сообщения @@ -902,8 +902,8 @@ impl TdClient { let reply_msg_id = MessageId::new(reply.message_id); self.current_chat_messages() .iter() - .find(|m| m.id == reply_msg_id) - .map(|m| m.sender_name.clone()) + .find(|m| m.id() == reply_msg_id) + .map(|m| m.sender_name().to_string()) .unwrap_or_else(|| "...".to_string()) }; @@ -917,8 +917,8 @@ impl TdClient { // Пробуем найти в текущих сообщениях self.current_chat_messages() .iter() - .find(|m| m.id == reply_msg_id) - .map(|m| m.content.clone()) + .find(|m| m.id() == reply_msg_id) + .map(|m| m.text().to_string()) .unwrap_or_default() }; @@ -996,7 +996,7 @@ impl TdClient { let msg_data: std::collections::HashMap = self .current_chat_messages() .iter() - .map(|m| (m.id, (m.sender_name.clone(), m.content.clone()))) + .map(|m| (m.id(), (m.sender_name().to_string(), m.text().to_string()))) .collect(); // Обновляем reply_to для сообщений с неполными данными diff --git a/src/tdlib/types.rs b/src/tdlib/types.rs index f7a0767..90c3952 100644 --- a/src/tdlib/types.rs +++ b/src/tdlib/types.rs @@ -57,17 +57,28 @@ pub struct ReactionInfo { pub is_chosen: bool, } +/// Метаданные сообщения (ID, отправитель, время) #[derive(Debug, Clone)] -pub struct MessageInfo { +pub struct MessageMetadata { pub id: MessageId, pub sender_name: String, - pub is_outgoing: bool, - pub content: String, - /// Сущности форматирования (bold, italic, code и т.д.) - pub entities: Vec, pub date: i32, /// Дата редактирования (0 если не редактировалось) pub edit_date: i32, +} + +/// Контент сообщения (текст и форматирование) +#[derive(Debug, Clone)] +pub struct MessageContent { + pub text: String, + /// Сущности форматирования (bold, italic, code и т.д.) + pub entities: Vec, +} + +/// Состояние и права доступа к сообщению +#[derive(Debug, Clone)] +pub struct MessageState { + pub is_outgoing: bool, pub is_read: bool, /// Можно ли редактировать сообщение pub can_be_edited: bool, @@ -75,6 +86,11 @@ pub struct MessageInfo { pub can_be_deleted_only_for_self: bool, /// Можно ли удалить для всех pub can_be_deleted_for_all_users: bool, +} + +/// Взаимодействия с сообщением (reply, forward, reactions) +#[derive(Debug, Clone)] +pub struct MessageInteractions { /// Информация о reply (если это ответ на сообщение) pub reply_to: Option, /// Информация о forward (если сообщение переслано) @@ -83,6 +99,120 @@ pub struct MessageInfo { pub reactions: Vec, } +#[derive(Debug, Clone)] +pub struct MessageInfo { + pub metadata: MessageMetadata, + pub content: MessageContent, + pub state: MessageState, + pub interactions: MessageInteractions, +} + +impl MessageInfo { + /// Создать новое сообщение + pub fn new( + id: MessageId, + sender_name: String, + is_outgoing: bool, + content: String, + entities: Vec, + date: i32, + edit_date: i32, + is_read: bool, + can_be_edited: bool, + can_be_deleted_only_for_self: bool, + can_be_deleted_for_all_users: bool, + reply_to: Option, + forward_from: Option, + reactions: Vec, + ) -> Self { + Self { + metadata: MessageMetadata { + id, + sender_name, + date, + edit_date, + }, + content: MessageContent { + text: content, + entities, + }, + state: MessageState { + is_outgoing, + is_read, + can_be_edited, + can_be_deleted_only_for_self, + can_be_deleted_for_all_users, + }, + interactions: MessageInteractions { + reply_to, + forward_from, + reactions, + }, + } + } + + // Удобные getter'ы для частых операций + pub fn id(&self) -> MessageId { + self.metadata.id + } + + pub fn sender_name(&self) -> &str { + &self.metadata.sender_name + } + + pub fn date(&self) -> i32 { + self.metadata.date + } + + pub fn edit_date(&self) -> i32 { + self.metadata.edit_date + } + + pub fn is_edited(&self) -> bool { + self.metadata.edit_date > 0 + } + + pub fn text(&self) -> &str { + &self.content.text + } + + pub fn entities(&self) -> &[TextEntity] { + &self.content.entities + } + + pub fn is_outgoing(&self) -> bool { + self.state.is_outgoing + } + + pub fn is_read(&self) -> bool { + self.state.is_read + } + + pub fn can_be_edited(&self) -> bool { + self.state.can_be_edited + } + + pub fn can_be_deleted_only_for_self(&self) -> bool { + self.state.can_be_deleted_only_for_self + } + + pub fn can_be_deleted_for_all_users(&self) -> bool { + self.state.can_be_deleted_for_all_users + } + + pub fn reply_to(&self) -> Option<&ReplyInfo> { + self.interactions.reply_to.as_ref() + } + + pub fn forward_from(&self) -> Option<&ForwardInfo> { + self.interactions.forward_from.as_ref() + } + + pub fn reactions(&self) -> &[ReactionInfo] { + &self.interactions.reactions + } +} + #[derive(Debug, Clone)] pub struct FolderInfo { pub id: i32, diff --git a/src/ui/messages.rs b/src/ui/messages.rs index a9487bd..1aba1f0 100644 --- a/src/ui/messages.rs +++ b/src/ui/messages.rs @@ -420,13 +420,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { // Pinned bar (если есть закреплённое сообщение) if let Some(pinned_msg) = &app.td_client.current_pinned_message() { - let pinned_preview: String = pinned_msg.content.chars().take(40).collect(); - let ellipsis = if pinned_msg.content.chars().count() > 40 { + let pinned_preview: String = pinned_msg.text().chars().take(40).collect(); + let ellipsis = if pinned_msg.text().chars().count() > 40 { "..." } else { "" }; - let pinned_datetime = crate::utils::format_datetime(pinned_msg.date); + let pinned_datetime = crate::utils::format_datetime(pinned_msg.date()); let pinned_text = format!("📌 {} {}{}", pinned_datetime, pinned_preview, ellipsis); let pinned_hint = "Ctrl+P"; @@ -454,26 +454,26 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { let mut last_sender: Option<(bool, String)> = None; // (is_outgoing, sender_name) // ID выбранного сообщения для подсветки - let selected_msg_id = app.get_selected_message().map(|m| m.id); + let selected_msg_id = app.get_selected_message().map(|m| m.id()); // Номер строки, где начинается выбранное сообщение (для автоскролла) let mut selected_msg_line: Option = None; for msg in app.td_client.current_chat_messages() { // Проверяем, выбрано ли это сообщение - let is_selected = selected_msg_id == Some(msg.id); + let is_selected = selected_msg_id == Some(msg.id()); // Запоминаем строку начала выбранного сообщения if is_selected { selected_msg_line = Some(lines.len()); } // Проверяем, нужно ли добавить разделитель даты - let msg_day = get_day(msg.date); + let msg_day = get_day(msg.date()); if last_day != Some(msg_day) { if last_day.is_some() { lines.push(Line::from("")); // Пустая строка перед разделителем } // Добавляем разделитель даты по центру - let date_str = format_date(msg.date); + let date_str = format_date(msg.date()); let date_line = format!("──────── {} ────────", date_str); let padding = content_width.saturating_sub(date_line.chars().count()) / 2; lines.push(Line::from(vec![ @@ -485,13 +485,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { last_sender = None; // Сбрасываем отправителя при смене дня } - let sender_name = if msg.is_outgoing { + let sender_name = if msg.is_outgoing() { "Вы".to_string() } else { - msg.sender_name.clone() + msg.sender_name().to_string() }; - let current_sender = (msg.is_outgoing, sender_name.clone()); + let current_sender = (msg.is_outgoing(), sender_name.clone()); // Проверяем, нужно ли показать заголовок отправителя let show_sender_header = last_sender.as_ref() != Some(¤t_sender); @@ -502,7 +502,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { lines.push(Line::from("")); } - let sender_style = if msg.is_outgoing { + let sender_style = if msg.is_outgoing() { Style::default() .fg(Color::Green) .add_modifier(Modifier::BOLD) @@ -512,7 +512,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { .add_modifier(Modifier::BOLD) }; - if msg.is_outgoing { + if msg.is_outgoing() { // Заголовок "Вы" справа let header_text = format!("{} ────────────────", sender_name); let header_len = header_text.chars().count(); @@ -534,12 +534,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } // Форматируем время (HH:MM) с учётом timezone из config - let time = format_timestamp_with_tz(msg.date, &app.config.general.timezone); + let time = format_timestamp_with_tz(msg.date(), &app.config.general.timezone); // Цвет сообщения (из config или жёлтый если выбрано) let msg_color = if is_selected { app.config.parse_color(&app.config.colors.selected_message) - } else if msg.is_outgoing { + } else if msg.is_outgoing() { app.config.parse_color(&app.config.colors.outgoing_message) } else { app.config.parse_color(&app.config.colors.incoming_message) @@ -550,11 +550,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { let marker_len = selection_marker.chars().count(); // Отображаем forward если есть - if let Some(forward) = &msg.forward_from { + if let Some(forward) = msg.forward_from() { let forward_line = format!("↪ Переслано от {}", forward.sender_name); let forward_len = forward_line.chars().count(); - if msg.is_outgoing { + if msg.is_outgoing() { // Forward справа для исходящих let padding = content_width.saturating_sub(forward_len + 1); lines.push(Line::from(vec![ @@ -571,7 +571,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } // Отображаем reply если есть - if let Some(reply) = &msg.reply_to { + if let Some(reply) = msg.reply_to() { let reply_text: String = reply.text.chars().take(40).collect(); let ellipsis = if reply.text.chars().count() > 40 { "..." @@ -581,7 +581,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { let reply_line = format!("┌ {}: {}{}", reply.sender_name, reply_text, ellipsis); let reply_len = reply_line.chars().count(); - if msg.is_outgoing { + if msg.is_outgoing() { // Reply справа для исходящих let padding = content_width.saturating_sub(reply_len + 1); lines.push(Line::from(vec![ @@ -597,17 +597,17 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } } - if msg.is_outgoing { + if msg.is_outgoing() { // Исходящие: справа, формат "текст (HH:MM ✎ ✓✓)" - let read_mark = if msg.is_read { "✓✓" } else { "✓" }; - let edit_mark = if msg.edit_date > 0 { "✎ " } else { "" }; + let read_mark = if msg.is_read() { "✓✓" } else { "✓" }; + let edit_mark = if msg.is_edited() { "✎ " } else { "" }; let time_mark = format!("({} {}{})", time, edit_mark, read_mark); let time_mark_len = time_mark.chars().count() + 1; // +1 для пробела // Максимальная ширина для текста сообщения (оставляем место для time_mark и маркера) let max_msg_width = content_width.saturating_sub(time_mark_len + marker_len + 2); - let wrapped_lines = wrap_text_with_offsets(&msg.content, max_msg_width); + let wrapped_lines = wrap_text_with_offsets(msg.text(), max_msg_width); let total_wrapped = wrapped_lines.len(); for (i, wrapped) in wrapped_lines.into_iter().enumerate() { @@ -616,7 +616,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { // Получаем entities для этой строки let line_entities = adjust_entities_for_substring( - &msg.entities, + msg.entities(), wrapped.start_offset, line_len, ); @@ -662,21 +662,21 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } } else { // Входящие: слева, формат "(HH:MM ✎) текст" - let edit_mark = if msg.edit_date > 0 { " ✎" } else { "" }; + let edit_mark = if msg.is_edited() { " ✎" } else { "" }; let time_str = format!("({}{})", time, edit_mark); let time_prefix_len = time_str.chars().count() + 2; // " (HH:MM) " // Максимальная ширина для текста let max_msg_width = content_width.saturating_sub(time_prefix_len + 1); - let wrapped_lines = wrap_text_with_offsets(&msg.content, max_msg_width); + let wrapped_lines = wrap_text_with_offsets(msg.text(), max_msg_width); for (i, wrapped) in wrapped_lines.into_iter().enumerate() { let line_len = wrapped.text.chars().count(); // Получаем entities для этой строки let line_entities = adjust_entities_for_substring( - &msg.entities, + msg.entities(), wrapped.start_offset, line_len, ); @@ -714,10 +714,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } // Отображаем реакции под сообщением - if !msg.reactions.is_empty() { + if !msg.reactions().is_empty() { let mut reaction_spans = vec![]; - for reaction in &msg.reactions { + for reaction in &msg.reactions() { if !reaction_spans.is_empty() { reaction_spans.push(Span::raw(" ")); } @@ -749,7 +749,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } // Выравниваем реакции в зависимости от типа сообщения - if msg.is_outgoing { + if msg.is_outgoing() { // Реакции справа для исходящих let reactions_text: String = reaction_spans .iter() @@ -815,8 +815,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { let forward_preview = app .get_forwarding_message() .map(|m| { - let text_preview: String = m.content.chars().take(40).collect(); - let ellipsis = if m.content.chars().count() > 40 { + let text_preview: String = m.text().chars().take(40).collect(); + let ellipsis = if m.text().chars().count() > 40 { "..." } else { "" @@ -875,10 +875,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { let sender = if m.is_outgoing { "Вы" } else { - &m.sender_name + m.sender_name() }; - let text_preview: String = m.content.chars().take(30).collect(); - let ellipsis = if m.content.chars().count() > 30 { + let text_preview: String = m.text().chars().take(30).collect(); + let ellipsis = if m.text().chars().count() > 30 { "..." } else { "" @@ -1056,15 +1056,15 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { // Маркер выбора, имя и дата let marker = if is_selected { "▶ " } else { " " }; - let sender_color = if msg.is_outgoing { + let sender_color = if msg.is_outgoing() { Color::Green } else { Color::Cyan }; - let sender_name = if msg.is_outgoing { + let sender_name = if msg.is_outgoing() { "Вы".to_string() } else { - msg.sender_name.clone() + msg.sender_name().to_string() }; lines.push(Line::from(vec![ @@ -1081,7 +1081,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { .add_modifier(Modifier::BOLD), ), Span::styled( - format!("({})", crate::utils::format_datetime(msg.date)), + format!("({})", crate::utils::format_datetime(msg.date())), Style::default().fg(Color::Gray), ), ])); @@ -1093,7 +1093,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { Color::White }; let max_width = content_width.saturating_sub(4); - let wrapped = wrap_text_with_offsets(&msg.content, max_width); + let wrapped = wrap_text_with_offsets(msg.text(), max_width); let wrapped_count = wrapped.len(); for wrapped_line in wrapped.into_iter().take(2) { @@ -1222,15 +1222,15 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { // Маркер выбора и имя отправителя let marker = if is_selected { "▶ " } else { " " }; - let sender_color = if msg.is_outgoing { + let sender_color = if msg.is_outgoing() { Color::Green } else { Color::Cyan }; - let sender_name = if msg.is_outgoing { + let sender_name = if msg.is_outgoing() { "Вы".to_string() } else { - msg.sender_name.clone() + msg.sender_name().to_string() }; lines.push(Line::from(vec![ @@ -1247,7 +1247,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { .add_modifier(Modifier::BOLD), ), Span::styled( - format!("({})", crate::utils::format_datetime(msg.date)), + format!("({})", crate::utils::format_datetime(msg.date())), Style::default().fg(Color::Gray), ), ])); @@ -1259,7 +1259,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { Color::White }; let max_width = content_width.saturating_sub(4); - let wrapped = wrap_text_with_offsets(&msg.content, max_width); + let wrapped = wrap_text_with_offsets(msg.text(), max_width); let wrapped_count = wrapped.len(); for wrapped_line in wrapped.into_iter().take(3) { diff --git a/tests/copy.rs b/tests/copy.rs index 41c15bd..02395de 100644 --- a/tests/copy.rs +++ b/tests/copy.rs @@ -106,7 +106,7 @@ fn format_message_for_test(msg: &tele_tui::tdlib::MessageInfo) -> String { } // Добавляем основной текст - result.push_str(&msg.content); + result.push_str(msg.text()); result } diff --git a/tests/delete_message.rs b/tests/delete_message.rs index b1bfaa9..d0a6a51 100644 --- a/tests/delete_message.rs +++ b/tests/delete_message.rs @@ -52,7 +52,7 @@ fn test_delete_multiple_messages() { let messages = client.get_messages(123); assert_eq!(messages.len(), 1); assert_eq!(messages[0].id, msg2_id); - assert_eq!(messages[0].content, "Message 2"); + assert_eq!(messages[0].content.text(), "Message 2"); } /// Test: Удаление только своих сообщений (проверка через can_be_deleted_for_all_users) @@ -145,5 +145,5 @@ fn test_cancel_delete_keeps_message() { // Сообщение на месте let messages = client.get_messages(123); assert_eq!(messages[0].id, msg_id); - assert_eq!(messages[0].content, "Keep me"); + assert_eq!(messages[0].content.text(), "Keep me"); } diff --git a/tests/edit_message.rs b/tests/edit_message.rs index a8918b0..b2eb42c 100644 --- a/tests/edit_message.rs +++ b/tests/edit_message.rs @@ -24,7 +24,7 @@ fn test_edit_message_changes_text() { // Проверяем что текст сообщения изменился let messages = client.get_messages(123); assert_eq!(messages.len(), 1); - assert_eq!(messages[0].content, "Edited text"); + assert_eq!(messages[0].content.text(), "Edited text"); } /// Test: Редактирование устанавливает edit_date @@ -97,7 +97,7 @@ fn test_multiple_edits_of_same_message() { // Проверяем что сообщение содержит последнюю версию let messages = client.get_messages(123); assert_eq!(messages.len(), 1); - assert_eq!(messages[0].content, "Final version"); + assert_eq!(messages[0].content.text(), "Final version"); } /// Test: Редактирование несуществующего сообщения (ничего не происходит) @@ -129,21 +129,21 @@ fn test_edit_history_tracking() { // Сохраняем original let messages_before = client.get_messages(123); - let original = messages_before[0].content.clone(); + let original = messages_before[0].text().to_string(); // Редактируем client.edit_message(123, msg_id, "Edited".to_string()); // Проверяем что изменилось let messages_edited = client.get_messages(123); - assert_eq!(messages_edited[0].content, "Edited"); + assert_eq!(messages_edited[0].content.text(), "Edited"); // Можем "отменить" редактирование вернув original client.edit_message(123, msg_id, original); // Проверяем что вернулось let messages_restored = client.get_messages(123); - assert_eq!(messages_restored[0].content, "Original"); + assert_eq!(messages_restored[0].content.text(), "Original"); // История показывает 2 редактирования assert_eq!(client.edited_messages().len(), 2); diff --git a/tests/helpers/fake_tdclient.rs b/tests/helpers/fake_tdclient.rs index a0575a4..210a70c 100644 --- a/tests/helpers/fake_tdclient.rs +++ b/tests/helpers/fake_tdclient.rs @@ -156,9 +156,9 @@ impl FakeTdClient { // Обновляем сообщение в списке if let Some(messages) = self.messages.get_mut(&chat_id) { - if let Some(msg) = messages.iter_mut().find(|m| m.id == message_id) { - msg.content = new_text; - msg.edit_date = msg.date + 60; + if let Some(msg) = messages.iter_mut().find(|m| m.id() == message_id) { + msg.content.text = new_text; + msg.metadata.edit_date = msg.metadata.date + 60; } } } diff --git a/tests/helpers/test_data.rs b/tests/helpers/test_data.rs index 252b253..88df4ff 100644 --- a/tests/helpers/test_data.rs +++ b/tests/helpers/test_data.rs @@ -188,22 +188,22 @@ impl TestMessageBuilder { } pub fn build(self) -> MessageInfo { - MessageInfo { - id: MessageId::new(self.id), - sender_name: self.sender_name, - is_outgoing: self.is_outgoing, - content: self.content, - entities: self.entities, - date: self.date, - edit_date: self.edit_date, - is_read: self.is_read, - can_be_edited: self.can_be_edited, - can_be_deleted_only_for_self: self.can_be_deleted_only_for_self, - can_be_deleted_for_all_users: self.can_be_deleted_for_all_users, - reply_to: self.reply_to, - forward_from: self.forward_from, - reactions: self.reactions, - } + MessageInfo::new( + MessageId::new(self.id), + self.sender_name, + self.is_outgoing, + self.content, + self.entities, + self.date, + self.edit_date, + self.is_read, + self.can_be_edited, + self.can_be_deleted_only_for_self, + self.can_be_deleted_for_all_users, + self.reply_to, + self.forward_from, + self.reactions, + ) } } diff --git a/tests/navigation.rs b/tests/navigation.rs index 51cae48..0cac41c 100644 --- a/tests/navigation.rs +++ b/tests/navigation.rs @@ -223,6 +223,6 @@ fn test_load_older_messages_on_scroll_up() { // Теперь должно быть 15 сообщений assert_eq!(client.get_messages(123).len(), 15); - assert_eq!(client.get_messages(123)[0].content, "Msg 81"); - assert_eq!(client.get_messages(123)[14].content, "Msg 100"); + assert_eq!(client.get_messages(123)[0].content.text(), "Msg 81"); + assert_eq!(client.get_messages(123)[14].content.text(), "Msg 100"); } diff --git a/tests/reply_forward.rs b/tests/reply_forward.rs index 842e2c3..a1f0697 100644 --- a/tests/reply_forward.rs +++ b/tests/reply_forward.rs @@ -29,7 +29,7 @@ fn test_reply_creates_message_with_reply_to() { let messages = client.get_messages(123); assert_eq!(messages.len(), 2); assert_eq!(messages[1].id, reply_id); - assert_eq!(messages[1].content, "Answer!"); + assert_eq!(messages[1].content.text(), "Answer!"); } /// Test: Reply отображает превью оригинального сообщения @@ -76,7 +76,7 @@ fn test_cancel_reply_sends_without_reply_to() { assert_eq!(client.sent_messages()[0].reply_to, None); let messages = client.get_messages(123); - assert_eq!(messages[1].content, "Regular message"); + assert_eq!(messages[1].content.text(), "Regular message"); } /// Test: Forward создаёт сообщение с forward_from diff --git a/tests/search.rs b/tests/search.rs index ed4db0e..60cdbbb 100644 --- a/tests/search.rs +++ b/tests/search.rs @@ -99,12 +99,12 @@ fn test_search_messages_in_chat() { let messages = client.get_messages(123); let found: Vec<_> = messages .iter() - .filter(|m| m.content.to_lowercase().contains(&query)) + .filter(|m| m.text().to_lowercase().contains(&query)) .collect(); assert_eq!(found.len(), 2); - assert_eq!(found[0].content, "Hello world"); - assert_eq!(found[1].content, "Hello again"); + assert_eq!(found[0].text(), "Hello world"); + assert_eq!(found[1].text(), "Hello again"); } /// Test: Навигация по результатам поиска (n/N) @@ -124,7 +124,7 @@ fn test_navigate_search_results() { let results: Vec<_> = messages .iter() .enumerate() - .filter(|(_, m)| m.content.to_lowercase().contains(&query)) + .filter(|(_, m)| m.text().to_lowercase().contains(&query)) .collect(); assert_eq!(results.len(), 3); @@ -135,17 +135,17 @@ fn test_navigate_search_results() { // n - следующий результат current_index = (current_index + 1) % results.len(); assert_eq!(current_index, 1); - assert_eq!(results[current_index].1.content, "Second match"); + assert_eq!(results[current_index].1.text(), "Second match"); // n - ещё один current_index = (current_index + 1) % results.len(); assert_eq!(current_index, 2); - assert_eq!(results[current_index].1.content, "Third match"); + assert_eq!(results[current_index].1.text(), "Third match"); // n - wrap around к первому current_index = (current_index + 1) % results.len(); assert_eq!(current_index, 0); - assert_eq!(results[current_index].1.content, "First match"); + assert_eq!(results[current_index].1.text(), "First match"); // N - предыдущий (wrap to last) current_index = if current_index == 0 { @@ -154,7 +154,7 @@ fn test_navigate_search_results() { current_index - 1 }; assert_eq!(current_index, 2); - assert_eq!(results[current_index].1.content, "Third match"); + assert_eq!(results[current_index].1.text(), "Third match"); } /// Test: Поиск с учётом регистра (case-insensitive) @@ -173,7 +173,7 @@ fn test_search_case_insensitive() { let messages = client.get_messages(123); let found: Vec<_> = messages .iter() - .filter(|m| m.content.to_lowercase().contains(&query)) + .filter(|m| m.text().to_lowercase().contains(&query)) .collect(); // Все 3 варианта должны найтись @@ -195,7 +195,7 @@ fn test_search_no_results() { let messages = client.get_messages(123); let found: Vec<_> = messages .iter() - .filter(|m| m.content.to_lowercase().contains(&query)) + .filter(|m| m.text().to_lowercase().contains(&query)) .collect(); assert_eq!(found.len(), 0); diff --git a/tests/send_message.rs b/tests/send_message.rs index 2ddbc8c..7110ddc 100644 --- a/tests/send_message.rs +++ b/tests/send_message.rs @@ -25,7 +25,7 @@ fn test_send_text_message() { let messages = client.get_messages(123); assert_eq!(messages.len(), 1); assert_eq!(messages[0].id, msg_id); - assert_eq!(messages[0].content, "Hello, Mom!"); + assert_eq!(messages.text(), "Hello, Mom!"); assert_eq!(messages[0].is_outgoing, true); } @@ -52,9 +52,9 @@ fn test_send_multiple_messages_updates_list() { assert_eq!(messages[0].id, msg1_id); assert_eq!(messages[1].id, msg2_id); assert_eq!(messages[2].id, msg3_id); - assert_eq!(messages[0].content, "Message 1"); - assert_eq!(messages[1].content, "Message 2"); - assert_eq!(messages[2].content, "Message 3"); + assert_eq!(messages.text(), "Message 1"); + assert_eq!(messages.text(), "Message 2"); + assert_eq!(messages.text(), "Message 3"); } /// Test: Отправка пустого сообщения (должно быть игнорировано на уровне App) @@ -74,7 +74,7 @@ fn test_send_empty_message_technical() { let messages = client.get_messages(123); assert_eq!(messages.len(), 1); assert_eq!(messages[0].id, msg_id); - assert_eq!(messages[0].content, ""); + assert_eq!(messages.text(), ""); } /// Test: Отправка сообщения с форматированием (markdown сущности) @@ -89,7 +89,7 @@ fn test_send_message_with_markdown() { // Проверяем что текст сохранился как есть (парсинг markdown - отдельная логика) let messages = client.get_messages(123); assert_eq!(messages.len(), 1); - assert_eq!(messages[0].content, text); + assert_eq!(messages.text(), text); } /// Test: Отправка сообщения в разные чаты @@ -112,12 +112,12 @@ fn test_send_messages_to_different_chats() { // Проверяем что сообщения распределены по чатам let chat123_messages = client.get_messages(123); assert_eq!(chat123_messages.len(), 2); - assert_eq!(chat123_messages[0].content, "Hello Mom"); - assert_eq!(chat123_messages[1].content, "How are you?"); + assert_eq!(chat123_messages.text(), "Hello Mom"); + assert_eq!(chat123_messages.text(), "How are you?"); let chat456_messages = client.get_messages(456); assert_eq!(chat456_messages.len(), 1); - assert_eq!(chat456_messages[0].content, "Hello Boss"); + assert_eq!(chat456_messages.text(), "Hello Boss"); } /// Test: Новое сообщение появляется в реальном времени (симуляция) @@ -141,6 +141,6 @@ fn test_receive_incoming_message() { assert_eq!(messages.len(), 2); assert_eq!(messages[0].is_outgoing, true); // Наше сообщение assert_eq!(messages[1].is_outgoing, false); // Входящее - assert_eq!(messages[1].content, "Hey there!"); + assert_eq!(messages.text(), "Hey there!"); assert_eq!(messages[1].sender_name, "Alice"); }