fix(images): eliminate race condition when pressing v on downloading photo #26
@@ -87,6 +87,7 @@ impl<T: TdClientTrait> NavigationMethods<T> for App<T> {
|
|||||||
#[cfg(feature = "images")]
|
#[cfg(feature = "images")]
|
||||||
{
|
{
|
||||||
self.photo_download_rx = None;
|
self.photo_download_rx = None;
|
||||||
|
self.pending_image_open = None;
|
||||||
}
|
}
|
||||||
// Сбрасываем состояние чата в нормальный режим
|
// Сбрасываем состояние чата в нормальный режим
|
||||||
self.chat_state = ChatState::Normal;
|
self.chat_state = ChatState::Normal;
|
||||||
|
|||||||
@@ -20,6 +20,19 @@ use crate::types::ChatId;
|
|||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Pending intent to open the image modal once a photo finishes downloading.
|
||||||
|
///
|
||||||
|
/// Set when the user presses `v` on a photo that is still downloading.
|
||||||
|
/// The main loop opens the modal automatically when the download completes.
|
||||||
|
#[cfg(feature = "images")]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PendingImageOpen {
|
||||||
|
pub file_id: i32,
|
||||||
|
pub message_id: crate::types::MessageId,
|
||||||
|
pub photo_width: i32,
|
||||||
|
pub photo_height: i32,
|
||||||
|
}
|
||||||
|
|
||||||
/// State of the account switcher modal overlay.
|
/// State of the account switcher modal overlay.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum AccountSwitcherState {
|
pub enum AccountSwitcherState {
|
||||||
@@ -123,6 +136,9 @@ pub struct App<T: TdClientTrait = TdClient> {
|
|||||||
/// Время последнего рендеринга изображений (для throttling до 15 FPS)
|
/// Время последнего рендеринга изображений (для throttling до 15 FPS)
|
||||||
#[cfg(feature = "images")]
|
#[cfg(feature = "images")]
|
||||||
pub last_image_render_time: Option<std::time::Instant>,
|
pub last_image_render_time: Option<std::time::Instant>,
|
||||||
|
/// Pending intent: открыть модалку для этого фото когда загрузится
|
||||||
|
#[cfg(feature = "images")]
|
||||||
|
pub pending_image_open: Option<PendingImageOpen>,
|
||||||
// Account lock
|
// Account lock
|
||||||
/// Advisory file lock to prevent concurrent access to the same account
|
/// Advisory file lock to prevent concurrent access to the same account
|
||||||
pub account_lock: Option<std::fs::File>,
|
pub account_lock: Option<std::fs::File>,
|
||||||
@@ -219,6 +235,8 @@ impl<T: TdClientTrait> App<T> {
|
|||||||
image_modal: None,
|
image_modal: None,
|
||||||
#[cfg(feature = "images")]
|
#[cfg(feature = "images")]
|
||||||
last_image_render_time: None,
|
last_image_render_time: None,
|
||||||
|
#[cfg(feature = "images")]
|
||||||
|
pending_image_open: None,
|
||||||
// Voice playback
|
// Voice playback
|
||||||
audio_player: crate::audio::AudioPlayer::new().ok(),
|
audio_player: crate::audio::AudioPlayer::new().ok(),
|
||||||
voice_cache: crate::audio::VoiceCache::new(audio_cache_size_mb).ok(),
|
voice_cache: crate::audio::VoiceCache::new(audio_cache_size_mb).ok(),
|
||||||
|
|||||||
@@ -584,45 +584,44 @@ async fn handle_view_image<T: TdClientTrait>(app: &mut App<T>) {
|
|||||||
});
|
});
|
||||||
app.needs_redraw = true;
|
app.needs_redraw = true;
|
||||||
}
|
}
|
||||||
PhotoDownloadState::Downloading => {
|
PhotoDownloadState::NotDownloaded | PhotoDownloadState::Downloading => {
|
||||||
app.status_message = Some("Загрузка фото...".to_string());
|
// Запоминаем намерение открыть модалку — откроется когда загрузится
|
||||||
}
|
app.pending_image_open = Some(crate::app::PendingImageOpen {
|
||||||
PhotoDownloadState::NotDownloaded => {
|
file_id,
|
||||||
// Скачиваем фото и открываем
|
message_id: msg_id,
|
||||||
|
photo_width,
|
||||||
|
photo_height,
|
||||||
|
});
|
||||||
app.status_message = Some("Загрузка фото...".to_string());
|
app.status_message = Some("Загрузка фото...".to_string());
|
||||||
app.needs_redraw = true;
|
app.needs_redraw = true;
|
||||||
match app.td_client.download_file(file_id).await {
|
|
||||||
Ok(path) => {
|
// Если нет активной фоновой загрузки — запускаем свою
|
||||||
// Обновляем состояние загрузки в сообщении
|
if app.photo_download_rx.is_none() {
|
||||||
for msg in app.td_client.current_chat_messages_mut() {
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
if let Some(photo) = msg.photo_info_mut() {
|
app.photo_download_rx = Some(rx);
|
||||||
if photo.file_id == file_id {
|
let client_id = app.td_client.client_id();
|
||||||
photo.download_state = PhotoDownloadState::Downloaded(path.clone());
|
tokio::spawn(async move {
|
||||||
break;
|
let result = tokio::time::timeout(Duration::from_secs(30), async {
|
||||||
|
match tdlib_rs::functions::download_file(
|
||||||
|
file_id, 1, 0, 0, true, client_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(tdlib_rs::enums::File::File(f))
|
||||||
|
if f.local.is_downloading_completed
|
||||||
|
&& !f.local.path.is_empty() =>
|
||||||
|
{
|
||||||
|
Ok(f.local.path)
|
||||||
}
|
}
|
||||||
|
Ok(_) => Err("Файл не скачан".to_string()),
|
||||||
|
Err(e) => Err(format!("{:?}", e)),
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
// Открываем модалку
|
.await;
|
||||||
app.image_modal = Some(ImageModalState {
|
let result =
|
||||||
message_id: msg_id,
|
result.unwrap_or_else(|_| Err("Таймаут загрузки".to_string()));
|
||||||
photo_path: path,
|
let _ = tx.send((file_id, result));
|
||||||
photo_width,
|
});
|
||||||
photo_height,
|
|
||||||
});
|
|
||||||
app.status_message = None;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
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::Error(e.clone());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app.error_message = Some(format!("Ошибка загрузки фото: {}", e));
|
|
||||||
app.status_message = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PhotoDownloadState::Error(_) => {
|
PhotoDownloadState::Error(_) => {
|
||||||
|
|||||||
41
src/main.rs
41
src/main.rs
@@ -216,6 +216,47 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Если это фото ждёт открытия в модалке — открываем
|
||||||
|
let pending_matches = app
|
||||||
|
.pending_image_open
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.file_id == file_id)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if pending_matches {
|
||||||
|
// Ищем путь из обновлённого состояния
|
||||||
|
let downloaded_path = app
|
||||||
|
.td_client
|
||||||
|
.current_chat_messages()
|
||||||
|
.iter()
|
||||||
|
.find_map(|m| {
|
||||||
|
m.photo_info().and_then(|p| {
|
||||||
|
if p.file_id == file_id {
|
||||||
|
if let PhotoDownloadState::Downloaded(ref path) =
|
||||||
|
p.download_state
|
||||||
|
{
|
||||||
|
Some(path.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if let (Some(path), Some(pending)) =
|
||||||
|
(downloaded_path, app.pending_image_open.take())
|
||||||
|
{
|
||||||
|
use crate::tdlib::ImageModalState;
|
||||||
|
app.image_modal = Some(ImageModalState {
|
||||||
|
message_id: pending.message_id,
|
||||||
|
photo_path: path,
|
||||||
|
photo_width: pending.photo_width,
|
||||||
|
photo_height: pending.photo_height,
|
||||||
|
});
|
||||||
|
app.status_message = None;
|
||||||
|
got_photos = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if got_photos {
|
if got_photos {
|
||||||
|
|||||||
Reference in New Issue
Block a user