/// Модуль для бизнес-логики работы с сообщениями /// /// Чёткое разделение ответственности: /// - `tdlib/messages.rs` - только получение и преобразование из TDLib /// - `app/message_service.rs` (этот модуль) - бизнес-логика и операции /// - `ui/messages.rs` - только рендеринг /// /// Этот модуль отвечает за: /// - Группировку сообщений по дате и отправителю /// - Фильтрацию сообщений /// - Поиск внутри сообщений /// - Навигацию по сообщениям /// - Операции над сообщениями (edit, delete, reply и т.д.) use crate::tdlib::MessageInfo; use crate::types::MessageId; use chrono::{DateTime, Local}; use std::collections::HashMap; /// Группа сообщений по дате #[derive(Debug, Clone)] pub struct MessageGroup { /// Дата группы (отображаемая строка, например "Сегодня", "Вчера", "1 января") pub date_label: String, /// Сообщения в этой группе (отсортированы по времени) pub messages: Vec, } /// Подгруппа сообщений от одного отправителя #[derive(Debug, Clone)] pub struct SenderGroup { /// ID первого сообщения в группе pub first_message_id: MessageId, /// Имя отправителя pub sender_name: String, /// Список ID сообщений от этого отправителя подряд pub message_ids: Vec, } /// Результат поиска сообщений #[derive(Debug, Clone)] pub struct MessageSearchResult { /// ID сообщения pub message_id: MessageId, /// Позиция в списке сообщений pub index: usize, /// Фрагмент текста с совпадением pub snippet: String, /// Позиция совпадения в тексте pub match_position: usize, } /// Сервис для работы с сообщениями pub struct MessageService; impl MessageService { /// Группирует сообщения по дате /// /// # Arguments /// /// * `messages` - Список сообщений (должен быть отсортирован по времени) /// * `timezone_offset` - Смещение часового пояса в секундах /// /// # Returns /// /// Список групп сообщений по датам pub fn group_by_date( messages: &[MessageInfo], timezone_offset: i32, ) -> Vec { let mut groups: Vec = Vec::new(); let mut current_date: Option = None; let mut current_messages: Vec = Vec::new(); for msg in messages { let date_label = Self::get_date_label(msg.date(), timezone_offset); if current_date.as_ref() != Some(&date_label) { // Начинается новая дата - сохраняем предыдущую группу if let Some(date) = current_date { groups.push(MessageGroup { date_label: date, messages: current_messages.clone(), }); current_messages.clear(); } current_date = Some(date_label); } current_messages.push(msg.id()); } // Добавляем последнюю группу if let Some(date) = current_date { groups.push(MessageGroup { date_label: date, messages: current_messages, }); } groups } /// Группирует сообщения по отправителю внутри одной даты /// /// Последовательные сообщения от одного отправителя объединяются в группу. pub fn group_by_sender(messages: &[MessageInfo]) -> Vec { let mut groups: Vec = Vec::new(); let mut current_sender: Option = None; let mut current_ids: Vec = Vec::new(); let mut first_id: Option = None; for msg in messages { let sender = msg.sender_name().to_string(); if current_sender.as_ref() != Some(&sender) { // Новый отправитель - сохраняем предыдущую группу if let (Some(name), Some(first)) = (current_sender, first_id) { groups.push(SenderGroup { first_message_id: first, sender_name: name, message_ids: current_ids.clone(), }); current_ids.clear(); } current_sender = Some(sender); first_id = Some(msg.id()); } current_ids.push(msg.id()); } // Добавляем последнюю группу if let (Some(name), Some(first)) = (current_sender, first_id) { groups.push(SenderGroup { first_message_id: first, sender_name: name, message_ids: current_ids, }); } groups } /// Получает человекочитаемую метку даты /// /// Возвращает "Сегодня", "Вчера" или дату в формате "1 января 2024" fn get_date_label(timestamp: i32, _timezone_offset: i32) -> String { let dt = DateTime::from_timestamp(timestamp as i64, 0) .map(|dt| dt.with_timezone(&Local)) .unwrap_or_else(|| Local::now()); let msg_date = dt.date_naive(); let today = Local::now().date_naive(); let yesterday = today.pred_opt().unwrap_or(today); if msg_date == today { "Сегодня".to_string() } else if msg_date == yesterday { "Вчера".to_string() } else { msg_date.format("%d %B %Y").to_string() } } /// Ищет сообщения по текстовому запросу /// /// # Arguments /// /// * `messages` - Список сообщений для поиска /// * `query` - Поисковый запрос (case-insensitive) /// * `max_results` - Максимальное количество результатов (0 = без ограничений) /// /// # Returns /// /// Список результатов поиска с контекстом pub fn search( messages: &[MessageInfo], query: &str, max_results: usize, ) -> Vec { if query.is_empty() { return Vec::new(); } let query_lower = query.to_lowercase(); let mut results = Vec::new(); for (index, msg) in messages.iter().enumerate() { let text = msg.text().to_lowercase(); if let Some(pos) = text.find(&query_lower) { // Создаём snippet с контекстом let start = pos.saturating_sub(20); let end = (pos + query.len() + 20).min(text.len()); let snippet = msg.text()[start..end].to_string(); results.push(MessageSearchResult { message_id: msg.id(), index, snippet, match_position: pos, }); if max_results > 0 && results.len() >= max_results { break; } } } results } /// Находит следующее сообщение по запросу /// /// # Arguments /// /// * `messages` - Список сообщений /// * `current_index` - Текущая позиция /// * `query` - Поисковый запрос /// /// # Returns /// /// Индекс следующего найденного сообщения или None pub fn find_next( messages: &[MessageInfo], current_index: usize, query: &str, ) -> Option { if query.is_empty() { return None; } let query_lower = query.to_lowercase(); for (index, msg) in messages.iter().enumerate().skip(current_index + 1) { if msg.text().to_lowercase().contains(&query_lower) { return Some(index); } } None } /// Находит предыдущее сообщение по запросу pub fn find_previous( messages: &[MessageInfo], current_index: usize, query: &str, ) -> Option { if query.is_empty() || current_index == 0 { return None; } let query_lower = query.to_lowercase(); for (index, msg) in messages.iter().enumerate().take(current_index).rev() { if msg.text().to_lowercase().contains(&query_lower) { return Some(index); } } None } /// Фильтрует сообщения по отправителю pub fn filter_by_sender<'a>( messages: &'a [MessageInfo], sender_name: &str, ) -> Vec<&'a MessageInfo> { messages .iter() .filter(|msg| msg.sender_name() == sender_name) .collect() } /// Фильтрует только непрочитанные сообщения pub fn filter_unread<'a>( messages: &'a [MessageInfo], last_read_id: MessageId, ) -> Vec<&'a MessageInfo> { messages .iter() .filter(|msg| msg.id().as_i64() > last_read_id.as_i64()) .collect() } /// Находит сообщение по ID pub fn find_by_id<'a>( messages: &'a [MessageInfo], id: MessageId, ) -> Option<&'a MessageInfo> { messages.iter().find(|msg| msg.id() == id) } /// Находит индекс сообщения по ID pub fn find_index_by_id( messages: &[MessageInfo], id: MessageId, ) -> Option { messages.iter().position(|msg| msg.id() == id) } /// Получает N последних сообщений pub fn get_last_n<'a>( messages: &'a [MessageInfo], n: usize, ) -> &'a [MessageInfo] { let start = messages.len().saturating_sub(n); &messages[start..] } /// Получает сообщения в диапазоне дат pub fn get_in_date_range<'a>( messages: &'a [MessageInfo], start_date: i32, end_date: i32, ) -> Vec<&'a MessageInfo> { messages .iter() .filter(|msg| { let date = msg.date(); date >= start_date && date <= end_date }) .collect() } /// Подсчитывает сообщения по типу отправителя pub fn count_by_sender_type(messages: &[MessageInfo]) -> (usize, usize) { let mut incoming = 0; let mut outgoing = 0; for msg in messages { if msg.is_outgoing() { outgoing += 1; } else { incoming += 1; } } (incoming, outgoing) } /// Создаёт индекс сообщений по ID для быстрого доступа pub fn create_index(messages: &[MessageInfo]) -> HashMap { messages .iter() .enumerate() .map(|(index, msg)| (msg.id(), index)) .collect() } } #[cfg(test)] mod tests { use super::*; use crate::tdlib::MessageInfo; use crate::types::MessageId; fn create_test_message( id: i64, text: &str, sender: &str, date: i32, is_outgoing: bool, ) -> MessageInfo { MessageInfo::new( MessageId::new(id), sender.to_string(), is_outgoing, text.to_string(), Vec::new(), // entities date, 0, // edit_date true, // is_read is_outgoing, // can_be_edited only for outgoing true, // can_be_deleted_only_for_self is_outgoing, // can_be_deleted_for_all_users only for outgoing None, // reply_to None, // forward_from Vec::new(), // reactions ) } #[test] fn test_search() { let messages = vec![ create_test_message(1, "Hello world", "Alice", 1000, false), create_test_message(2, "How are you?", "Bob", 1010, false), create_test_message(3, "Hello there", "Alice", 1020, false), ]; let results = MessageService::search(&messages, "hello", 0); assert_eq!(results.len(), 2); assert_eq!(results[0].message_id.as_i64(), 1); assert_eq!(results[1].message_id.as_i64(), 3); // Case-insensitive let results = MessageService::search(&messages, "HELLO", 0); assert_eq!(results.len(), 2); // Max results let results = MessageService::search(&messages, "hello", 1); assert_eq!(results.len(), 1); } #[test] fn test_find_next_previous() { let messages = vec![ create_test_message(1, "test 1", "Alice", 1000, false), create_test_message(2, "message", "Bob", 1010, false), create_test_message(3, "test 2", "Alice", 1020, false), create_test_message(4, "test 3", "Bob", 1030, false), ]; // Find next let next = MessageService::find_next(&messages, 0, "test"); assert_eq!(next, Some(2)); let next = MessageService::find_next(&messages, 2, "test"); assert_eq!(next, Some(3)); // Find previous let prev = MessageService::find_previous(&messages, 3, "test"); assert_eq!(prev, Some(2)); let prev = MessageService::find_previous(&messages, 2, "test"); assert_eq!(prev, Some(0)); } #[test] fn test_filter_by_sender() { let messages = vec![ create_test_message(1, "msg1", "Alice", 1000, false), create_test_message(2, "msg2", "Bob", 1010, false), create_test_message(3, "msg3", "Alice", 1020, false), ]; let filtered = MessageService::filter_by_sender(&messages, "Alice"); assert_eq!(filtered.len(), 2); assert_eq!(filtered[0].id().as_i64(), 1); assert_eq!(filtered[1].id().as_i64(), 3); } #[test] fn test_find_by_id() { let messages = vec![ create_test_message(1, "msg1", "Alice", 1000, false), create_test_message(2, "msg2", "Bob", 1010, false), ]; let found = MessageService::find_by_id(&messages, MessageId::new(2)); assert!(found.is_some()); assert_eq!(found.unwrap().text(), "msg2"); let not_found = MessageService::find_by_id(&messages, MessageId::new(999)); assert!(not_found.is_none()); } #[test] fn test_count_by_sender_type() { let messages = vec![ create_test_message(1, "msg1", "Alice", 1000, false), create_test_message(2, "msg2", "Me", 1010, true), create_test_message(3, "msg3", "Bob", 1020, false), create_test_message(4, "msg4", "Me", 1030, true), ]; let (incoming, outgoing) = MessageService::count_by_sender_type(&messages); assert_eq!(incoming, 2); assert_eq!(outgoing, 2); } #[test] fn test_get_last_n() { let messages = vec![ create_test_message(1, "msg1", "Alice", 1000, false), create_test_message(2, "msg2", "Bob", 1010, false), create_test_message(3, "msg3", "Alice", 1020, false), ]; let last_2 = MessageService::get_last_n(&messages, 2); assert_eq!(last_2.len(), 2); assert_eq!(last_2[0].id().as_i64(), 2); assert_eq!(last_2[1].id().as_i64(), 3); // Request more than available let last_10 = MessageService::get_last_n(&messages, 10); assert_eq!(last_10.len(), 3); } #[test] fn test_create_index() { let messages = vec![ create_test_message(1, "msg1", "Alice", 1000, false), create_test_message(2, "msg2", "Bob", 1010, false), create_test_message(3, "msg3", "Alice", 1020, false), ]; let index = MessageService::create_index(&messages); assert_eq!(index.len(), 3); assert_eq!(index.get(&MessageId::new(1)), Some(&0)); assert_eq!(index.get(&MessageId::new(2)), Some(&1)); assert_eq!(index.get(&MessageId::new(3)), Some(&2)); } }