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

@@ -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<i64> {
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<i64> {
self.get_selected_search_result().map(|m| m.id)
self.get_selected_search_result().map(|m| m.id())
}
/// Получить поисковый запрос из режима поиска

View File

@@ -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
}

View File

@@ -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<i64, (String, String)> = 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 для сообщений с неполными данными

View File

@@ -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<TextEntity>,
pub date: i32,
/// Дата редактирования (0 если не редактировалось)
pub edit_date: i32,
}
/// Контент сообщения (текст и форматирование)
#[derive(Debug, Clone)]
pub struct MessageContent {
pub text: String,
/// Сущности форматирования (bold, italic, code и т.д.)
pub entities: Vec<TextEntity>,
}
/// Состояние и права доступа к сообщению
#[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<ReplyInfo>,
/// Информация о forward (если сообщение переслано)
@@ -83,6 +99,120 @@ pub struct MessageInfo {
pub reactions: Vec<ReactionInfo>,
}
#[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<TextEntity>,
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<ReplyInfo>,
forward_from: Option<ForwardInfo>,
reactions: Vec<ReactionInfo>,
) -> 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,

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) {

View File

@@ -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
}

View File

@@ -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");
}

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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,
)
}
}

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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");
}