Files
telegram-tui/src/message_grouping.rs
Mikhail Kilin d4e1ed1376
Some checks failed
ci/woodpecker/pr/check Pipeline failed
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
fix: resolve all 23 clippy warnings
2026-02-22 17:28:50 +03:00

448 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Модуль для группировки сообщений по дате и отправителю
//!
//! Предоставляет функции для логической группировки сообщений
//! перед отображением, отделяя логику группировки от рендеринга.
use crate::tdlib::MessageInfo;
use crate::utils::get_day;
/// Элемент группированного списка сообщений
#[derive(Debug, Clone)]
pub enum MessageGroup {
/// Разделитель даты (день в формате timestamp)
DateSeparator(i32),
/// Заголовок отправителя (is_outgoing, sender_name)
SenderHeader {
is_outgoing: bool,
sender_name: String,
},
/// Сообщение
Message(Box<MessageInfo>),
/// Альбом (группа фото с одинаковым media_album_id)
Album(Vec<MessageInfo>),
}
/// Группирует сообщения по дате и отправителю
///
/// # Аргументы
///
/// * `messages` - Список сообщений для группировки
///
/// # Возвращает
///
/// Вектор `MessageGroup` с разделителями дат, заголовками отправителей и сообщениями
///
/// # Примеры
///
/// ```no_run
/// use tele_tui::message_grouping::{group_messages, MessageGroup};
///
/// # use tele_tui::tdlib::types::MessageBuilder;
/// # use tele_tui::types::MessageId;
/// # let msg = MessageBuilder::new(MessageId::new(1)).sender_name("Alice").text("Hello").build();
/// let messages = vec![msg];
/// let grouped = group_messages(&messages);
///
/// for group in grouped {
/// match group {
/// MessageGroup::DateSeparator(_day) => {
/// // Рендерим разделитель даты
/// }
/// MessageGroup::SenderHeader { is_outgoing, sender_name } => {
/// // Рендерим заголовок отправителя
/// println!("{}: {}", if is_outgoing { "Outgoing" } else { "Incoming" }, sender_name);
/// }
/// MessageGroup::Message(msg) => {
/// // Рендерим сообщение
/// println!("{}", msg.text());
/// }
/// MessageGroup::Album(messages) => {
/// // Рендерим альбом (группу фото)
/// println!("Album with {} photos", messages.len());
/// }
/// }
/// }
/// ```
pub fn group_messages(messages: &[MessageInfo]) -> Vec<MessageGroup> {
let mut result = Vec::new();
let mut last_day: Option<i64> = None;
let mut last_sender: Option<(bool, String)> = None; // (is_outgoing, sender_name)
let mut album_acc: Vec<MessageInfo> = Vec::new();
/// Сбрасывает аккумулятор альбома в результат
fn flush_album(acc: &mut Vec<MessageInfo>, result: &mut Vec<MessageGroup>) {
if acc.is_empty() {
return;
}
if acc.len() >= 2 {
result.push(MessageGroup::Album(std::mem::take(acc)));
} else {
// Одно сообщение — не альбом
result.push(MessageGroup::Message(Box::new(acc.remove(0))));
}
}
for msg in messages {
// Проверяем, нужно ли добавить разделитель даты
let msg_day = get_day(msg.date());
if last_day != Some(msg_day) {
// Flush аккумулятор перед разделителем даты
flush_album(&mut album_acc, &mut result);
// Добавляем разделитель даты
result.push(MessageGroup::DateSeparator(msg.date()));
last_day = Some(msg_day);
last_sender = None; // Сбрасываем отправителя при смене дня
}
let sender_name = if msg.is_outgoing() {
"Вы".to_string()
} else {
msg.sender_name().to_string()
};
let current_sender = (msg.is_outgoing(), sender_name.clone());
// Проверяем, нужно ли показать заголовок отправителя
let show_sender_header = last_sender.as_ref() != Some(&current_sender);
if show_sender_header {
// Flush аккумулятор перед сменой отправителя
flush_album(&mut album_acc, &mut result);
result.push(MessageGroup::SenderHeader { is_outgoing: msg.is_outgoing(), sender_name });
last_sender = Some(current_sender);
}
// Проверяем, является ли сообщение частью альбома
let album_id = msg.media_album_id();
if album_id != 0 {
// Проверяем, совпадает ли album_id с текущим аккумулятором
if let Some(first) = album_acc.first() {
if first.media_album_id() == album_id {
// Тот же альбом — добавляем
album_acc.push(msg.clone());
continue;
} else {
// Другой альбом — flush старый, начинаем новый
flush_album(&mut album_acc, &mut result);
album_acc.push(msg.clone());
continue;
}
} else {
// Аккумулятор пуст — начинаем новый альбом
album_acc.push(msg.clone());
continue;
}
}
// Обычное сообщение (не альбом) — flush аккумулятор
flush_album(&mut album_acc, &mut result);
result.push(MessageGroup::Message(Box::new(msg.clone())));
}
// Flush оставшийся аккумулятор
flush_album(&mut album_acc, &mut result);
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tdlib::types::MessageBuilder;
use crate::types::MessageId;
#[test]
fn test_group_messages_by_date() {
// Создаём сообщения с разными датами
let msg1 = MessageBuilder::new(MessageId::new(1))
.sender_name("Alice")
.text("Message 1")
.date(1609459200) // 2021-01-01 00:00:00 UTC
.incoming()
.build();
let msg2 = MessageBuilder::new(MessageId::new(2))
.sender_name("Alice")
.text("Message 2")
.date(1609545600) // 2021-01-02 00:00:00 UTC
.incoming()
.build();
let messages = vec![msg1, msg2];
let grouped = group_messages(&messages);
// Должно быть: DateSep, SenderHeader, Message, DateSep, SenderHeader, Message
assert_eq!(grouped.len(), 6);
assert!(matches!(grouped[0], MessageGroup::DateSeparator(_)));
assert!(matches!(grouped[1], MessageGroup::SenderHeader { .. }));
assert!(matches!(grouped[2], MessageGroup::Message(_)));
assert!(matches!(grouped[3], MessageGroup::DateSeparator(_)));
assert!(matches!(grouped[4], MessageGroup::SenderHeader { .. }));
assert!(matches!(grouped[5], MessageGroup::Message(_)));
}
#[test]
fn test_group_messages_by_sender() {
// Создаём сообщения от разных отправителей
let msg1 = MessageBuilder::new(MessageId::new(1))
.sender_name("Alice")
.text("Message 1")
.date(1609459200)
.incoming()
.build();
let msg2 = MessageBuilder::new(MessageId::new(2))
.sender_name("Alice")
.text("Message 2")
.date(1609459300) // +100 секунд, тот же день
.incoming()
.build();
let msg3 = MessageBuilder::new(MessageId::new(3))
.sender_name("Bob")
.text("Message 3")
.date(1609459400)
.incoming()
.build();
let messages = vec![msg1, msg2, msg3];
let grouped = group_messages(&messages);
// Должно быть: DateSep, SenderHeader(Alice), Message, Message, SenderHeader(Bob), Message
assert_eq!(grouped.len(), 6);
assert!(matches!(grouped[0], MessageGroup::DateSeparator(_)));
if let MessageGroup::SenderHeader { sender_name, .. } = &grouped[1] {
assert_eq!(sender_name, "Alice");
} else {
panic!("Expected SenderHeader");
}
assert!(matches!(grouped[2], MessageGroup::Message(_)));
assert!(matches!(grouped[3], MessageGroup::Message(_)));
if let MessageGroup::SenderHeader { sender_name, .. } = &grouped[4] {
assert_eq!(sender_name, "Bob");
} else {
panic!("Expected SenderHeader");
}
assert!(matches!(grouped[5], MessageGroup::Message(_)));
}
#[test]
fn test_group_outgoing_vs_incoming() {
// Проверяем группировку исходящих и входящих сообщений
let msg1 = MessageBuilder::new(MessageId::new(1))
.sender_name("Alice")
.text("Hello")
.date(1609459200)
.incoming()
.build();
let msg2 = MessageBuilder::new(MessageId::new(2))
.sender_name("Me")
.text("Hi")
.date(1609459300)
.outgoing()
.build();
let messages = vec![msg1, msg2];
let grouped = group_messages(&messages);
// Должно быть: DateSep, SenderHeader(Alice), Message, SenderHeader(Me), Message
assert_eq!(grouped.len(), 5);
if let MessageGroup::SenderHeader { is_outgoing, sender_name } = &grouped[1] {
assert_eq!(*is_outgoing, false);
assert_eq!(sender_name, "Alice");
} else {
panic!("Expected SenderHeader");
}
if let MessageGroup::SenderHeader { is_outgoing, sender_name } = &grouped[3] {
assert_eq!(*is_outgoing, true);
assert_eq!(sender_name, "Вы");
} else {
panic!("Expected SenderHeader");
}
}
#[test]
fn test_empty_messages() {
let messages: Vec<MessageInfo> = vec![];
let grouped = group_messages(&messages);
assert_eq!(grouped.len(), 0);
}
#[test]
fn test_single_message() {
let msg = MessageBuilder::new(MessageId::new(1))
.sender_name("Alice")
.text("Single message")
.date(1609459200)
.incoming()
.build();
let messages = vec![msg];
let grouped = group_messages(&messages);
// Должно быть: DateSep, SenderHeader, Message
assert_eq!(grouped.len(), 3);
assert!(matches!(grouped[0], MessageGroup::DateSeparator(_)));
assert!(matches!(grouped[1], MessageGroup::SenderHeader { .. }));
assert!(matches!(grouped[2], MessageGroup::Message(_)));
}
#[test]
fn test_album_grouping_two_photos() {
let msg1 = MessageBuilder::new(MessageId::new(1))
.sender_name("Alice")
.text("Photo 1")
.date(1609459200)
.incoming()
.media_album_id(12345)
.build();
let msg2 = MessageBuilder::new(MessageId::new(2))
.sender_name("Alice")
.text("Photo 2")
.date(1609459201)
.incoming()
.media_album_id(12345)
.build();
let messages = vec![msg1, msg2];
let grouped = group_messages(&messages);
// DateSep, SenderHeader, Album
assert_eq!(grouped.len(), 3);
assert!(matches!(grouped[0], MessageGroup::DateSeparator(_)));
assert!(matches!(grouped[1], MessageGroup::SenderHeader { .. }));
if let MessageGroup::Album(album) = &grouped[2] {
assert_eq!(album.len(), 2);
assert_eq!(album[0].id(), MessageId::new(1));
assert_eq!(album[1].id(), MessageId::new(2));
} else {
panic!("Expected Album, got {:?}", grouped[2]);
}
}
#[test]
fn test_album_single_photo_not_album() {
// Одно сообщение с album_id → не альбом, обычное сообщение
let msg = MessageBuilder::new(MessageId::new(1))
.sender_name("Alice")
.text("Single photo")
.date(1609459200)
.incoming()
.media_album_id(12345)
.build();
let messages = vec![msg];
let grouped = group_messages(&messages);
// DateSep, SenderHeader, Message (не Album)
assert_eq!(grouped.len(), 3);
assert!(matches!(grouped[2], MessageGroup::Message(_)));
}
#[test]
fn test_album_with_regular_messages() {
let msg1 = MessageBuilder::new(MessageId::new(1))
.sender_name("Alice")
.text("Text message")
.date(1609459200)
.incoming()
.build();
let msg2 = MessageBuilder::new(MessageId::new(2))
.sender_name("Alice")
.text("Photo 1")
.date(1609459201)
.incoming()
.media_album_id(100)
.build();
let msg3 = MessageBuilder::new(MessageId::new(3))
.sender_name("Alice")
.text("Photo 2")
.date(1609459202)
.incoming()
.media_album_id(100)
.build();
let msg4 = MessageBuilder::new(MessageId::new(4))
.sender_name("Alice")
.text("After album")
.date(1609459203)
.incoming()
.build();
let messages = vec![msg1, msg2, msg3, msg4];
let grouped = group_messages(&messages);
// DateSep, SenderHeader, Message, Album, Message
assert_eq!(grouped.len(), 5);
assert!(matches!(grouped[2], MessageGroup::Message(_)));
assert!(matches!(grouped[3], MessageGroup::Album(_)));
assert!(matches!(grouped[4], MessageGroup::Message(_)));
}
#[test]
fn test_two_different_albums() {
let msg1 = MessageBuilder::new(MessageId::new(1))
.sender_name("Alice")
.text("Album 1 - Photo 1")
.date(1609459200)
.incoming()
.media_album_id(100)
.build();
let msg2 = MessageBuilder::new(MessageId::new(2))
.sender_name("Alice")
.text("Album 1 - Photo 2")
.date(1609459201)
.incoming()
.media_album_id(100)
.build();
let msg3 = MessageBuilder::new(MessageId::new(3))
.sender_name("Alice")
.text("Album 2 - Photo 1")
.date(1609459202)
.incoming()
.media_album_id(200)
.build();
let msg4 = MessageBuilder::new(MessageId::new(4))
.sender_name("Alice")
.text("Album 2 - Photo 2")
.date(1609459203)
.incoming()
.media_album_id(200)
.build();
let messages = vec![msg1, msg2, msg3, msg4];
let grouped = group_messages(&messages);
// DateSep, SenderHeader, Album(2), Album(2)
assert_eq!(grouped.len(), 4);
if let MessageGroup::Album(a1) = &grouped[2] {
assert_eq!(a1.len(), 2);
assert_eq!(a1[0].media_album_id(), 100);
} else {
panic!("Expected first Album");
}
if let MessageGroup::Album(a2) = &grouped[3] {
assert_eq!(a2.len(), 2);
assert_eq!(a2[0].media_album_id(), 200);
} else {
panic!("Expected second Album");
}
}
}