feat: implement photo albums (media groups) and persist account selection
Group photos with shared media_album_id into single album bubbles with grid layout (up to 3x cols). Album navigation treats grouped photos as one unit (j/k skip entire album). Persist selected account to accounts.toml so it survives app restart. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,7 @@ pub struct TestMessageBuilder {
|
||||
reply_to: Option<ReplyInfo>,
|
||||
forward_from: Option<ForwardInfo>,
|
||||
reactions: Vec<ReactionInfo>,
|
||||
media_album_id: i64,
|
||||
}
|
||||
|
||||
impl TestMessageBuilder {
|
||||
@@ -134,6 +135,7 @@ impl TestMessageBuilder {
|
||||
reply_to: None,
|
||||
forward_from: None,
|
||||
reactions: vec![],
|
||||
media_album_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,8 +189,13 @@ impl TestMessageBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn media_album_id(mut self, id: i64) -> Self {
|
||||
self.media_album_id = id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> MessageInfo {
|
||||
MessageInfo::new(
|
||||
let mut msg = MessageInfo::new(
|
||||
MessageId::new(self.id),
|
||||
self.sender_name,
|
||||
self.is_outgoing,
|
||||
@@ -203,7 +210,9 @@ impl TestMessageBuilder {
|
||||
self.reply_to,
|
||||
self.forward_from,
|
||||
self.reactions,
|
||||
)
|
||||
);
|
||||
msg.metadata.media_album_id = self.media_album_id;
|
||||
msg
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -288,6 +288,137 @@ async fn test_normal_mode_auto_enters_message_selection() {
|
||||
assert!(app.is_selecting_message());
|
||||
}
|
||||
|
||||
/// Test: j/k перескакивают через альбом как одно сообщение
|
||||
#[tokio::test]
|
||||
async fn test_album_navigation_skips_grouped_messages() {
|
||||
let messages = vec![
|
||||
TestMessageBuilder::new("Before album", 1).sender("Alice").build(),
|
||||
TestMessageBuilder::new("Photo 1", 2)
|
||||
.sender("Alice")
|
||||
.media_album_id(100)
|
||||
.build(),
|
||||
TestMessageBuilder::new("Photo 2", 3)
|
||||
.sender("Alice")
|
||||
.media_album_id(100)
|
||||
.build(),
|
||||
TestMessageBuilder::new("Photo 3", 4)
|
||||
.sender("Alice")
|
||||
.media_album_id(100)
|
||||
.build(),
|
||||
TestMessageBuilder::new("After album", 5).sender("Alice").build(),
|
||||
];
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chats(vec![create_test_chat("Chat 1", 101)])
|
||||
.selected_chat(101)
|
||||
.with_messages(101, messages)
|
||||
.build();
|
||||
|
||||
// Входим в режим выбора — начинаем с последнего (index=4, "After album")
|
||||
app.start_message_selection();
|
||||
assert!(app.is_selecting_message());
|
||||
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "After album");
|
||||
|
||||
// k (up) — перескакиваем альбом, попадаем на первый элемент альбома (index=1)
|
||||
app.select_previous_message();
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "Photo 1");
|
||||
assert_eq!(msg.media_album_id(), 100);
|
||||
|
||||
// k (up) — перескакиваем на сообщение до альбома (index=0)
|
||||
app.select_previous_message();
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "Before album");
|
||||
|
||||
// j (down) — перескакиваем на первый элемент альбома (index=1)
|
||||
app.select_next_message();
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "Photo 1");
|
||||
|
||||
// j (down) — перескакиваем альбом, попадаем на "After album" (index=4)
|
||||
app.select_next_message();
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "After album");
|
||||
}
|
||||
|
||||
/// Test: Начало выбора, когда последнее сообщение — часть альбома
|
||||
#[tokio::test]
|
||||
async fn test_album_navigation_start_at_album_end() {
|
||||
let messages = vec![
|
||||
TestMessageBuilder::new("Regular", 1).sender("Alice").build(),
|
||||
TestMessageBuilder::new("Album Photo 1", 2)
|
||||
.sender("Alice")
|
||||
.media_album_id(200)
|
||||
.build(),
|
||||
TestMessageBuilder::new("Album Photo 2", 3)
|
||||
.sender("Alice")
|
||||
.media_album_id(200)
|
||||
.build(),
|
||||
];
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chats(vec![create_test_chat("Chat 1", 101)])
|
||||
.selected_chat(101)
|
||||
.with_messages(101, messages)
|
||||
.build();
|
||||
|
||||
// Входим в режим выбора — должны оказаться на первом элементе альбома (index=1)
|
||||
app.start_message_selection();
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "Album Photo 1");
|
||||
|
||||
// k (up) — на обычное сообщение
|
||||
app.select_previous_message();
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "Regular");
|
||||
}
|
||||
|
||||
/// Test: Два альбома подряд — навигация между ними
|
||||
#[tokio::test]
|
||||
async fn test_album_navigation_two_albums() {
|
||||
let messages = vec![
|
||||
TestMessageBuilder::new("A1-P1", 1)
|
||||
.sender("Alice")
|
||||
.media_album_id(100)
|
||||
.build(),
|
||||
TestMessageBuilder::new("A1-P2", 2)
|
||||
.sender("Alice")
|
||||
.media_album_id(100)
|
||||
.build(),
|
||||
TestMessageBuilder::new("A2-P1", 3)
|
||||
.sender("Alice")
|
||||
.media_album_id(200)
|
||||
.build(),
|
||||
TestMessageBuilder::new("A2-P2", 4)
|
||||
.sender("Alice")
|
||||
.media_album_id(200)
|
||||
.build(),
|
||||
];
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chats(vec![create_test_chat("Chat 1", 101)])
|
||||
.selected_chat(101)
|
||||
.with_messages(101, messages)
|
||||
.build();
|
||||
|
||||
// Начинаем — последний альбом (index=2, первый элемент album 200)
|
||||
app.start_message_selection();
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "A2-P1");
|
||||
|
||||
// k — перескакиваем на первый альбом (index=0)
|
||||
app.select_previous_message();
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "A1-P1");
|
||||
|
||||
// j — перескакиваем на второй альбом (index=2)
|
||||
app.select_next_message();
|
||||
let msg = app.get_selected_message().unwrap();
|
||||
assert_eq!(msg.text(), "A2-P1");
|
||||
}
|
||||
|
||||
/// Test: Циклическая навигация по списку чатов (переход с конца в начало)
|
||||
#[tokio::test]
|
||||
async fn test_circular_navigation_optional() {
|
||||
|
||||
@@ -387,3 +387,118 @@ fn snapshot_selected_message() {
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("selected_message", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_album_incoming() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let msg1 = TestMessageBuilder::new("📷 [Фото]", 1)
|
||||
.sender("Alice")
|
||||
.media_album_id(12345)
|
||||
.build();
|
||||
let msg2 = TestMessageBuilder::new("Caption for album", 2)
|
||||
.sender("Alice")
|
||||
.media_album_id(12345)
|
||||
.build();
|
||||
let msg3 = TestMessageBuilder::new("📷 [Фото]", 3)
|
||||
.sender("Alice")
|
||||
.media_album_id(12345)
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1, msg2, msg3])
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("album_incoming", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_album_outgoing() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let msg1 = TestMessageBuilder::new("📷 [Фото]", 1)
|
||||
.outgoing()
|
||||
.media_album_id(99999)
|
||||
.build();
|
||||
let msg2 = TestMessageBuilder::new("My vacation photos", 2)
|
||||
.outgoing()
|
||||
.media_album_id(99999)
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1, msg2])
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("album_outgoing", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_album_with_regular_messages() {
|
||||
let chat = create_test_chat("Group Chat", 123);
|
||||
let msg1 = TestMessageBuilder::new("Regular message before", 1)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
let msg2 = TestMessageBuilder::new("📷 [Фото]", 2)
|
||||
.sender("Alice")
|
||||
.media_album_id(555)
|
||||
.build();
|
||||
let msg3 = TestMessageBuilder::new("Album caption", 3)
|
||||
.sender("Alice")
|
||||
.media_album_id(555)
|
||||
.build();
|
||||
let msg4 = TestMessageBuilder::new("Regular message after", 4)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1, msg2, msg3, msg4])
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("album_with_regular_messages", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_album_selected() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let msg1 = TestMessageBuilder::new("📷 [Фото]", 1)
|
||||
.sender("Alice")
|
||||
.media_album_id(777)
|
||||
.build();
|
||||
let msg2 = TestMessageBuilder::new("📷 [Фото]", 2)
|
||||
.sender("Alice")
|
||||
.media_album_id(777)
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1, msg2])
|
||||
.selected_chat(123)
|
||||
.selecting_message(1) // Выбираем одно из сообщений альбома
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("album_selected", output);
|
||||
}
|
||||
|
||||
28
tests/snapshots/messages__album_incoming.snap
Normal file
28
tests/snapshots/messages__album_incoming.snap
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
│ (14:33) Caption for album │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
28
tests/snapshots/messages__album_outgoing.snap
Normal file
28
tests/snapshots/messages__album_outgoing.snap
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ 📷 [Фото] (14:33 ✓✓)│
|
||||
│ My vacation photos (14:33 ✓✓) │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
28
tests/snapshots/messages__album_selected.snap
Normal file
28
tests/snapshots/messages__album_selected.snap
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
│▶ (14:33) 📷 [Фото] │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌ Выбор сообщения ─────────────────────────────────────────────────────────────┐
|
||||
│↑↓ · r ответ · f переслать · y копир. · d удалить · Esc │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
28
tests/snapshots/messages__album_with_regular_messages.snap
Normal file
28
tests/snapshots/messages__album_with_regular_messages.snap
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Group Chat │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 02.01.2022 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) Regular message before │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
│ (14:33) Album caption │
|
||||
│ (14:33) Regular message after │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
Reference in New Issue
Block a user