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:
Mikhail Kilin
2026-02-22 16:18:04 +03:00
parent 8bd08318bb
commit 78fe09bf11
18 changed files with 1011 additions and 30 deletions

View File

@@ -182,6 +182,34 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
app.needs_redraw = true;
}
// Обрабатываем результаты фоновой загрузки фото
#[cfg(feature = "images")]
{
use crate::tdlib::PhotoDownloadState;
let mut got_photos = false;
if let Some(ref mut rx) = app.photo_download_rx {
while let Ok((file_id, result)) = rx.try_recv() {
let new_state = match result {
Ok(path) => PhotoDownloadState::Downloaded(path),
Err(_) => PhotoDownloadState::Error("Ошибка загрузки".to_string()),
};
for msg in app.td_client.current_chat_messages_mut() {
if let Some(photo) = msg.photo_info_mut() {
if photo.file_id == file_id {
photo.download_state = new_state;
got_photos = true;
break;
}
}
}
}
}
if got_photos {
app.needs_redraw = true;
}
}
// Очищаем устаревший typing status
if app.td_client.clear_stale_typing_status() {
app.needs_redraw = true;
@@ -315,7 +343,7 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
)
.await;
// Авто-загрузка фото (последние 30 сообщений)
// Авто-загрузка фото — неблокирующая фоновая задача (до 5 фото параллельно)
#[cfg(feature = "images")]
{
use crate::tdlib::PhotoDownloadState;
@@ -326,7 +354,7 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
.current_chat_messages()
.iter()
.rev()
.take(30)
.take(5)
.filter_map(|msg| {
msg.photo_info().and_then(|p| {
matches!(p.download_state, PhotoDownloadState::NotDownloaded)
@@ -335,22 +363,42 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
})
.collect();
for file_id in &photo_file_ids {
if let Ok(Ok(path)) = tokio::time::timeout(
Duration::from_secs(5),
app.td_client.download_file(*file_id),
)
.await
{
for msg in app.td_client.current_chat_messages_mut() {
if let Some(photo) = msg.photo_info_mut() {
if photo.file_id == *file_id {
photo.download_state =
PhotoDownloadState::Downloaded(path);
break;
}
}
}
if !photo_file_ids.is_empty() {
let client_id = app.td_client.client_id();
let (tx, rx) =
tokio::sync::mpsc::unbounded_channel::<(i32, Result<String, String>)>();
app.photo_download_rx = Some(rx);
for file_id in photo_file_ids {
let tx = tx.clone();
tokio::spawn(async move {
let result = tokio::time::timeout(
Duration::from_secs(5),
async {
match tdlib_rs::functions::download_file(
file_id, 1, 0, 0, true, client_id,
)
.await
{
Ok(tdlib_rs::enums::File::File(file))
if file.local.is_downloading_completed
&& !file.local.path.is_empty() =>
{
Ok(file.local.path)
}
Ok(_) => Err("Файл не скачан".to_string()),
Err(e) => Err(format!("{:?}", e)),
}
},
)
.await;
let result = match result {
Ok(r) => r,
Err(_) => Err("Таймаут загрузки".to_string()),
};
let _ = tx.send((file_id, result));
});
}
}
}
@@ -371,8 +419,15 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
}
// 3. Reset app state
app.current_account_name = account_name;
app.current_account_name = account_name.clone();
app.screen = AppScreen::Loading;
// 4. Persist selected account as default for next launch
let mut accounts_config = accounts::load_or_create();
accounts_config.default_account = account_name;
if let Err(e) = accounts::save(&accounts_config) {
tracing::warn!("Could not save default account: {}", e);
}
app.chats.clear();
app.selected_chat_id = None;
app.chat_state = Default::default();