refactor: restructure MessageInfo with logical field grouping (P2.6)

Сгруппированы 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 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-01-31 01:45:54 +03:00
parent 7081a886ad
commit 43960332d9
14 changed files with 274 additions and 144 deletions

View File

@@ -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<usize> = 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(&current_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) {