diff --git a/docs/iamb.5 b/docs/iamb.5 index 8357f965..335f6708 100644 --- a/docs/iamb.5 +++ b/docs/iamb.5 @@ -138,6 +138,13 @@ Enable image previews and configure it. An empty object will enable the feature with default settings, omitting it will disable the feature. The available fields in this object are: .Bl -tag -width Ds +.It Sy lazy_load +If +.Sy true +(the default), download and render image previews when viewing a message with an image. +If +.Sy false , +load previews as soon as a message with an image is received. .It Sy size An optional object with .Sy width @@ -538,8 +545,6 @@ Configured as an object under the key .It Sy cache Specifies where to store assets and temporary data in. (For example, -.Sy image_preview -and .Sy logs will also go in here by default.) Defaults to @@ -555,11 +560,6 @@ Specifies where to store downloaded files. Defaults to .Ev $XDG_DOWNLOAD_DIR . -.It Sy image_previews -Specifies where to store automatically downloaded image previews. -Defaults to -.Ev ${cache}/image_preview_downloads . - .It Sy logs Specifies where to store log files. Defaults to diff --git a/src/base.rs b/src/base.rs index d191b809..8da67cd4 100644 --- a/src/base.rs +++ b/src/base.rs @@ -91,9 +91,8 @@ use modalkit::{ }; use crate::config::ImagePreviewProtocolValues; -use crate::message::ImageStatus; use crate::notifications::NotificationHandle; -use crate::preview::{source_from_event, spawn_insert_preview}; +use crate::preview::{source_from_event, PreviewManager}; use crate::{ message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages}, worker::Requester, @@ -1233,33 +1232,23 @@ impl RoomInfo { } } - /// Insert a new message event, and spawn a task for image-preview if it has an image - /// attachment. + /// Insert a new message event, and prepare for image-preview if it has an image attachment. pub fn insert_with_preview( &mut self, - room_id: OwnedRoomId, - store: AsyncProgramStore, - picker: Option, ev: RoomMessageEvent, - settings: &mut ApplicationSettings, - media: matrix_sdk::Media, + settings: &ApplicationSettings, + previews: &mut PreviewManager, + worker: &Requester, ) { - let source = picker.and_then(|_| source_from_event(&ev)); + let source = source_from_event(&ev); self.insert(ev); if let Some((event_id, source)) = source { if let (Some(msg), Some(image_preview)) = (self.get_event_mut(&event_id), &settings.tunables.image_preview) { - msg.image_preview = ImageStatus::Downloading(image_preview.size.clone()); - spawn_insert_preview( - store, - room_id, - event_id, - source, - media, - settings.dirs.image_previews.clone(), - ) + msg.image_preview = Some(source.clone()); + previews.register_preview(settings, source, image_preview.size, worker) } } } @@ -1601,8 +1590,8 @@ pub struct ChatStore { /// Information gathered by the background thread. pub sync_info: SyncInfo, - /// Image preview "protocol" picker. - pub picker: Option, + /// Rendered image previews. + pub previews: PreviewManager, /// Last draw time, used to match with RoomInfo's draw_last. pub draw_curr: Option, @@ -1628,7 +1617,7 @@ impl ChatStore { ChatStore { worker, settings, - picker, + previews: PreviewManager::new(picker), cmds: crate::commands::setup_commands(), emojis: emoji_map(), diff --git a/src/config.rs b/src/config.rs index e7a4a47b..b16dfa8a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -489,12 +489,14 @@ pub struct Notifications { #[derive(Clone)] pub struct ImagePreviewValues { + pub lazy_load: bool, pub size: ImagePreviewSize, pub protocol: Option, } #[derive(Clone, Default, Deserialize)] pub struct ImagePreview { + pub lazy_load: Option, pub size: Option, pub protocol: Option, } @@ -502,13 +504,14 @@ pub struct ImagePreview { impl ImagePreview { fn values(self) -> ImagePreviewValues { ImagePreviewValues { + lazy_load: self.lazy_load.unwrap_or(true), size: self.size.unwrap_or_default(), protocol: self.protocol, } } } -#[derive(Clone, Deserialize)] +#[derive(Clone, Copy, Deserialize, Debug)] pub struct ImagePreviewSize { pub width: usize, pub height: usize, @@ -683,19 +686,17 @@ pub struct DirectoryValues { pub data: PathBuf, pub logs: PathBuf, pub downloads: Option, - pub image_previews: PathBuf, } impl DirectoryValues { fn create_dir_all(&self) -> std::io::Result<()> { use std::fs::create_dir_all; - let Self { cache, data, logs, downloads, image_previews } = self; + let Self { cache, data, logs, downloads } = self; create_dir_all(cache)?; create_dir_all(data)?; create_dir_all(logs)?; - create_dir_all(image_previews)?; if let Some(downloads) = downloads { create_dir_all(downloads)?; @@ -711,7 +712,6 @@ pub struct Directories { pub data: Option, pub logs: Option, pub downloads: Option, - pub image_previews: Option, } impl Directories { @@ -721,7 +721,6 @@ impl Directories { data: self.data.or(other.data), logs: self.logs.or(other.logs), downloads: self.downloads.or(other.downloads), - image_previews: self.image_previews.or(other.image_previews), } } @@ -776,20 +775,7 @@ impl Directories { }) .or_else(dirs::download_dir); - let image_previews = self - .image_previews - .map(|dir| { - let dir = shellexpand::full(&dir) - .expect("unable to expand shell variables in dirs.cache"); - Path::new(dir.as_ref()).to_owned() - }) - .unwrap_or_else(|| { - let mut dir = cache.clone(); - dir.push("image_preview_downloads"); - dir - }); - - DirectoryValues { cache, data, logs, downloads, image_previews } + DirectoryValues { cache, data, logs, downloads } } } diff --git a/src/message/mod.rs b/src/message/mod.rs index 718c7a68..cdd6a04e 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -11,6 +11,7 @@ use std::ops::{Deref, DerefMut}; use chrono::{DateTime, Local as LocalTz}; use humansize::{format_size, DECIMAL}; use matrix_sdk::ruma::events::receipt::ReceiptThread; +use matrix_sdk::ruma::events::room::MediaSource; use matrix_sdk::ruma::room_version_rules::RedactionRules; use serde_json::json; use unicode_width::UnicodeWidthStr; @@ -58,6 +59,7 @@ use modalkit::prelude::*; use ratatui_image::protocol::Protocol; use crate::config::ImagePreviewSize; +use crate::preview::{ImageStatus, PreviewManager}; use crate::{ base::RoomInfo, config::ApplicationSettings, @@ -731,6 +733,7 @@ impl<'a> MessageFormatter<'a> { text: &mut Text<'a>, info: &'a RoomInfo, settings: &'a ApplicationSettings, + previews: &'a PreviewManager, ) -> Option> { let reply_style = if settings.tunables.message_user_color { style.patch(settings.get_user_color(&msg.sender)) @@ -740,7 +743,7 @@ impl<'a> MessageFormatter<'a> { let width = self.width(); let w = width.saturating_sub(2); - let (mut replied, proto) = msg.show_msg(w, reply_style, true, settings); + let (mut replied, proto) = msg.show_msg(w, reply_style, true, settings, previews); let mut sender = msg.sender_span(info, self.settings); let sender_width = UnicodeWidthStr::width(sender.content.as_ref()); let trailing = w.saturating_sub(sender_width + 1); @@ -841,20 +844,13 @@ impl<'a> MessageFormatter<'a> { } } -pub enum ImageStatus { - None, - Downloading(ImagePreviewSize), - Loaded(Protocol), - Error(String), -} - pub struct Message { pub event: MessageEvent, pub sender: OwnedUserId, pub timestamp: MessageTimeStamp, pub downloaded: bool, pub html: Option, - pub image_preview: ImageStatus, + pub image_preview: Option, } impl Message { @@ -868,7 +864,7 @@ impl Message { timestamp, downloaded, html, - image_preview: ImageStatus::None, + image_preview: None, } } @@ -893,7 +889,7 @@ impl Message { } } - fn thread_root(&self) -> Option { + pub fn thread_root(&self) -> Option { let content = match &self.event { MessageEvent::EncryptedOriginal(_) => return None, MessageEvent::EncryptedRedacted(_) => return None, @@ -1000,6 +996,7 @@ impl Message { vwctx: &ViewportContext, info: &'a RoomInfo, settings: &'a ApplicationSettings, + previews: &'a PreviewManager, ) -> (Text<'a>, [Option>; 2]) { let width = vwctx.get_width(); @@ -1015,11 +1012,11 @@ impl Message { .and_then(|e| info.get_event(&e)); let proto_reply = reply.as_ref().and_then(|r| { // Format the reply header, push it into the `Text` buffer, and get any image. - fmt.push_in_reply(r, style, &mut text, info, settings) + fmt.push_in_reply(r, style, &mut text, info, settings, previews) }); // Now show the message contents, and the inlined reply if we couldn't find it above. - let (msg, proto) = self.show_msg(width, style, reply.is_some(), settings); + let (msg, proto) = self.show_msg(width, style, reply.is_some(), settings, previews); // Given our text so far, determine the image offset. let proto_main = proto.map(|p| { @@ -1057,8 +1054,9 @@ impl Message { vwctx: &ViewportContext, info: &'a RoomInfo, settings: &'a ApplicationSettings, + previews: &'a PreviewManager, ) -> Text<'a> { - self.show_with_preview(prev, selected, vwctx, info, settings).0 + self.show_with_preview(prev, selected, vwctx, info, settings, previews).0 } fn show_msg<'a>( @@ -1067,6 +1065,7 @@ impl Message { style: Style, hide_reply: bool, settings: &'a ApplicationSettings, + previews: &'a PreviewManager, ) -> (Text<'a>, Option<&'a Protocol>) { if let Some(html) = &self.html { (html.to_text(width, style, hide_reply, settings), None) @@ -1081,17 +1080,21 @@ impl Message { } let mut proto = None; - let placeholder = match &self.image_preview { - ImageStatus::None => None, - ImageStatus::Downloading(image_preview_size) => { - placeholder_frame(Some("Downloading..."), width, image_preview_size) - }, - ImageStatus::Loaded(backend) => { - proto = Some(backend); - placeholder_frame(Some("No Space..."), width, &backend.area().into()) - }, - ImageStatus::Error(err) => Some(format!("[Image error: {err}]\n")), - }; + let placeholder = + match self.image_preview.as_ref().and_then(|source| previews.get(source)) { + None => None, + Some(ImageStatus::Queued(image_preview_size)) => { + placeholder_frame(Some("Queued..."), width, image_preview_size) + }, + Some(ImageStatus::Downloading(image_preview_size)) => { + placeholder_frame(Some("Downloading..."), width, image_preview_size) + }, + Some(ImageStatus::Loaded(backend)) => { + proto = Some(backend); + placeholder_frame(Some("No Space..."), width, &backend.area().into()) + }, + Some(ImageStatus::Error(err)) => Some(format!("[Image error: {err}]\n")), + }; if let Some(placeholder) = placeholder { msg.to_mut().insert_str(0, &placeholder); @@ -1143,7 +1146,7 @@ impl Message { self.event.redact(redaction, rules); self.html = None; self.downloaded = false; - self.image_preview = ImageStatus::None; + self.image_preview = None; } } diff --git a/src/preview.rs b/src/preview.rs index f2c620a5..6684a439 100644 --- a/src/preview.rs +++ b/src/preview.rs @@ -1,11 +1,7 @@ -use std::{ - fs::File, - io::{Read, Write}, - path::{Path, PathBuf}, -}; +use std::{collections::HashMap, sync::Arc}; use matrix_sdk::{ - media::{MediaFormat, MediaRequestParameters}, + media::{MediaFormat, MediaRequestParameters, UniqueKey}, ruma::{ events::{ room::{ @@ -15,19 +11,102 @@ use matrix_sdk::{ MessageLikeEvent, }, OwnedEventId, - OwnedRoomId, }, Media, }; use ratatui::layout::Rect; -use ratatui_image::Resize; +use ratatui_image::{picker::Picker, protocol::Protocol, Resize}; +use tokio::sync::Semaphore; use crate::{ - base::{AsyncProgramStore, ChatStore, IambError}, - config::ImagePreviewSize, - message::ImageStatus, + base::{AsyncProgramStore, IambError}, + config::{ApplicationSettings, ImagePreviewSize}, + worker::Requester, }; +pub enum ImageStatus { + Queued(ImagePreviewSize), + Downloading(ImagePreviewSize), + Loaded(Protocol), + Error(String), +} + +pub struct PreviewManager { + /// Image preview "protocol" picker. + picker: Option>, + + /// Permits for rendering images in background thread. + permits: Arc, + + /// Indexed by [`MediaSource::unique_key`] + previews: HashMap, +} + +impl PreviewManager { + pub fn new(picker: Option) -> Self { + Self { + picker: picker.map(Into::into), + permits: Arc::new(Semaphore::new(2)), + previews: Default::default(), + } + } + + pub fn get(&self, source: &MediaSource) -> Option<&ImageStatus> { + self.previews.get(&source.unique_key()) + } + + fn insert(&mut self, key: String, status: ImageStatus) { + self.previews.insert(key, status); + } + + /// Queue download and preparation of preview + pub fn load(&mut self, source: &MediaSource, worker: &Requester) { + let Some(status) = self.previews.get_mut(&source.unique_key()) else { + return; + }; + let Some(picker) = &self.picker else { return }; + + if let ImageStatus::Queued(size) = status { + let size = *size; + *status = ImageStatus::Downloading(size); + + worker.load_image( + source.to_owned(), + size.to_owned(), + Arc::clone(picker), + Arc::clone(&self.permits), + ); + } + } + + pub fn register_preview( + &mut self, + settings: &ApplicationSettings, + source: MediaSource, + size: ImagePreviewSize, + worker: &Requester, + ) { + if self.picker.is_none() { + return; + } + + let key = source.unique_key(); + if self.previews.contains_key(&key) { + return; + } + self.previews.insert(key, ImageStatus::Queued(size)); + + if settings + .tunables + .image_preview + .as_ref() + .is_some_and(|setting| !setting.lazy_load) + { + self.load(&source, worker); + } + } +} + pub fn source_from_event( ev: &MessageLikeEvent, ) -> Option<(OwnedEventId, MediaSource)> { @@ -50,126 +129,54 @@ impl From for ImagePreviewSize { } } -/// Download and prepare the preview, and then lock the store to insert it. -pub fn spawn_insert_preview( +pub async fn load_image( store: AsyncProgramStore, - room_id: OwnedRoomId, - event_id: OwnedEventId, - source: MediaSource, media: Media, - cache_dir: PathBuf, + source: MediaSource, + picker: Arc, + permits: Arc, + size: ImagePreviewSize, ) { - tokio::spawn(async move { - let img = download_or_load(event_id.to_owned(), source, media, cache_dir) + async fn load_image_inner( + media: Media, + source: MediaSource, + picker: Arc, + permits: Arc, + size: ImagePreviewSize, + ) -> Result { + let reader = media + .get_media_content(&MediaRequestParameters { source, format: MediaFormat::File }, true) .await .map(std::io::Cursor::new) .map(image::ImageReader::new) .map_err(IambError::Matrix) - .and_then(|reader| reader.with_guessed_format().map_err(IambError::IOError)) - .and_then(|reader| reader.decode().map_err(IambError::Image)); - - match img { - Err(err) => { - try_set_msg_preview_error( - &mut store.lock().await.application, - room_id, - event_id, - err, - ); - }, - Ok(img) => { - let mut locked = store.lock().await; - let ChatStore { rooms, picker, settings, .. } = &mut locked.application; - - match picker - .as_mut() - .ok_or_else(|| IambError::Preview("Picker is empty".to_string())) - .and_then(|picker| { - Ok(( - picker, - rooms - .get_or_default(room_id.clone()) - .get_event_mut(&event_id) - .ok_or_else(|| { - IambError::Preview("Message not found".to_string()) - })?, - settings.tunables.image_preview.clone().ok_or_else(|| { - IambError::Preview("image_preview settings not found".to_string()) - })?, - )) - }) - .and_then(|(picker, msg, image_preview)| { - picker - .new_protocol(img, image_preview.size.into(), Resize::Fit(None)) - .map_err(|err| IambError::Preview(format!("{err:?}"))) - .map(|backend| (backend, msg)) - }) { - Err(err) => { - try_set_msg_preview_error(&mut locked.application, room_id, event_id, err); - }, - Ok((backend, msg)) => { - msg.image_preview = ImageStatus::Loaded(backend); - }, - } - }, - } - }); -} + .and_then(|reader| reader.with_guessed_format().map_err(IambError::IOError))?; -fn try_set_msg_preview_error( - application: &mut ChatStore, - room_id: OwnedRoomId, - event_id: OwnedEventId, - err: IambError, -) { - let rooms = &mut application.rooms; - - match rooms - .get_or_default(room_id.clone()) - .get_event_mut(&event_id) - .ok_or_else(|| IambError::Preview("Message not found".to_string())) - { - Ok(msg) => msg.image_preview = ImageStatus::Error(format!("{err:?}")), - Err(err) => { - tracing::error!( - "Failed to set error on msg.image_backend for event {}, room {}: {}", - event_id, - room_id, - err - ) - }, - } -} + let image = reader.decode().map_err(IambError::Image)?; -async fn download_or_load( - event_id: OwnedEventId, - source: MediaSource, - media: Media, - mut cache_path: PathBuf, -) -> Result, matrix_sdk::Error> { - cache_path.push(Path::new(event_id.localpart())); - - match File::open(&cache_path) { - Ok(mut f) => { - let mut buffer = Vec::new(); - f.read_to_end(&mut buffer)?; - Ok(buffer) - }, - Err(_) => { - media - .get_media_content( - &MediaRequestParameters { source, format: MediaFormat::File }, - true, - ) - .await - .and_then(|buffer| { - if let Err(err) = - File::create(&cache_path).and_then(|mut f| f.write_all(&buffer)) - { - return Err(err.into()); - } - Ok(buffer) - }) - }, + let permit = permits + .acquire() + .await + .map_err(|err| IambError::Preview(err.to_string()))?; + + let handle = tokio::task::spawn_blocking(move || { + picker + .new_protocol(image, size.into(), Resize::Fit(None)) + .map_err(|err| IambError::Preview(err.to_string())) + }); + + let image = handle.await.map_err(|err| IambError::Preview(err.to_string()))??; + std::mem::drop(permit); + + Ok(ImageStatus::Loaded(image)) } + let key = source.unique_key(); + + let status = match load_image_inner(media, source, picker, permits, size).await { + Ok(status) => status, + Err(err) => ImageStatus::Error(format!("{err:?}")), + }; + + let mut locked = store.lock().await; + locked.application.previews.insert(key, status); } diff --git a/src/tests.rs b/src/tests.rs index e9b05021..bcf604ad 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -163,7 +163,6 @@ pub fn mock_dirs() -> DirectoryValues { data: PathBuf::new(), logs: PathBuf::new(), downloads: None, - image_previews: PathBuf::new(), } } diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs index 02340573..6f0e9960 100644 --- a/src/windows/room/scrollback.rs +++ b/src/windows/room/scrollback.rs @@ -55,6 +55,7 @@ use crate::{ }, config::ApplicationSettings, message::{Message, MessageCursor, MessageKey, Messages}, + preview::PreviewManager, }; fn no_msgs() -> EditError { @@ -270,6 +271,7 @@ impl ScrollbackState { pos: MovePosition, info: &RoomInfo, settings: &ApplicationSettings, + previews: &PreviewManager, ) { let Some(thread) = self.get_thread(info) else { return; @@ -292,7 +294,8 @@ impl ScrollbackState { for (key, item) in thread.range(..=&idx).rev() { let sel = selidx == key; let prev = prevmsg(key, thread); - let len = item.show(prev, sel, &self.viewctx, info, settings).lines.len(); + let len = + item.show(prev, sel, &self.viewctx, info, settings, previews).lines.len(); if key == &idx { lines += len / 2; @@ -315,7 +318,8 @@ impl ScrollbackState { for (key, item) in thread.range(..=&idx).rev() { let sel = key == selidx; let prev = prevmsg(key, thread); - let len = item.show(prev, sel, &self.viewctx, info, settings).lines.len(); + let len = + item.show(prev, sel, &self.viewctx, info, settings, previews).lines.len(); lines += len; @@ -338,7 +342,12 @@ impl ScrollbackState { self.jumped.push(self.cursor.clone()); } - fn shift_cursor(&mut self, info: &RoomInfo, settings: &ApplicationSettings) { + fn shift_cursor( + &mut self, + info: &RoomInfo, + settings: &ApplicationSettings, + previews: &PreviewManager, + ) { let Some(thread) = self.get_thread(info) else { return; }; @@ -368,7 +377,10 @@ impl ScrollbackState { break; } - lines += item.show(prev, false, &self.viewctx, info, settings).height().max(1); + lines += item + .show(prev, false, &self.viewctx, info, settings, previews) + .height() + .max(1); if lines >= self.viewctx.get_height() { // We've reached the end of the viewport; move cursor into it. @@ -1067,6 +1079,7 @@ impl ScrollActions for ScrollbackState { ) -> EditResult { let info = store.application.rooms.get_or_default(self.room_id.clone()); let settings = &store.application.settings; + let previews = &store.application.previews; let mut corner = self.viewctx.corner.clone(); let thread = self.get_thread(info).ok_or_else(no_msgs)?; @@ -1094,7 +1107,7 @@ impl ScrollActions for ScrollbackState { for (key, item) in thread.range(..=&corner_key).rev() { let sel = key == cursor_key; let prev = prevmsg(key, thread); - let txt = item.show(prev, sel, &self.viewctx, info, settings); + let txt = item.show(prev, sel, &self.viewctx, info, settings, previews); let len = txt.height().max(1); let max = len.saturating_sub(1); @@ -1122,7 +1135,7 @@ impl ScrollActions for ScrollbackState { for (key, item) in thread.range(&corner_key..) { let sel = key == cursor_key; - let txt = item.show(prev, sel, &self.viewctx, info, settings); + let txt = item.show(prev, sel, &self.viewctx, info, settings, previews); let len = txt.height().max(1); let max = len.saturating_sub(1); @@ -1160,7 +1173,7 @@ impl ScrollActions for ScrollbackState { } self.viewctx.corner = corner; - self.shift_cursor(info, settings); + self.shift_cursor(info, settings, previews); Ok(None) } @@ -1182,10 +1195,11 @@ impl ScrollActions for ScrollbackState { Axis::Vertical => { let info = store.application.rooms.get_or_default(self.room_id.clone()); let settings = &store.application.settings; + let previews = &store.application.previews; let thread = self.get_thread(info).ok_or_else(no_msgs)?; if let Some(key) = self.cursor.to_key(thread).cloned() { - self.scrollview(key, pos, info, settings); + self.scrollview(key, pos, info, settings, previews); } Ok(None) @@ -1345,10 +1359,33 @@ impl StatefulWidget for Scrollback<'_> { let mut sawit = false; let mut prev = prevmsg(&corner_key, thread); + // load image previews + for (_, item) in thread.range(&corner_key..).rev() { + if let Some(source) = &item.image_preview { + self.store + .application + .previews + .load(source, &self.store.application.worker); + } + let reply = item + .reply_to() + .or_else(|| item.thread_root()) + .and_then(|e| info.get_event(&e)) + .and_then(|msg| msg.image_preview.as_ref()); + if let Some(source) = reply { + self.store + .application + .previews + .load(source, &self.store.application.worker); + } + } + + let previews = &self.store.application.previews; for (key, item) in thread.range(&corner_key..) { let sel = key == cursor_key; + let (txt, [mut msg_preview, mut reply_preview]) = - item.show_with_preview(prev, foc && sel, &state.viewctx, info, settings); + item.show_with_preview(prev, foc && sel, &state.viewctx, info, settings, previews); let incomplete_ok = !full || !sel; diff --git a/src/worker.rs b/src/worker.rs index f3c9791a..e1fdaefc 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -13,6 +13,8 @@ use std::time::{Duration, Instant}; use futures::{stream::FuturesUnordered, StreamExt}; use gethostname::gethostname; +use matrix_sdk::ruma::events::room::MediaSource; +use ratatui_image::picker::Picker; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::sync::Semaphore; use tokio::task::JoinHandle; @@ -89,7 +91,9 @@ use modalkit::errors::UIError; use modalkit::prelude::{EditInfo, InfoMessage}; use crate::base::MessageNeed; +use crate::config::ImagePreviewSize; use crate::notifications::register_notifications; +use crate::preview::load_image; use crate::{ base::{ AsyncProgramStore, @@ -256,11 +260,10 @@ async fn run_plan(client: &Client, store: &AsyncProgramStore, plan: Plan, permit Plan::Messages(room_id, fetch_id, message_need) => { let limit = MIN_MSG_LOAD; let client = client.clone(); - let store_clone = store.clone(); let res = load_older_one(&client, &room_id, fetch_id, limit).await; let mut locked = store.lock().await; - load_insert(room_id, res, locked.deref_mut(), store_clone, message_need); + load_insert(room_id, res, locked.deref_mut(), message_need); }, Plan::Members(room_id) => { let res = members_load(client, &room_id).await; @@ -322,13 +325,11 @@ fn load_insert( room_id: OwnedRoomId, res: MessageFetchResult, locked: &mut ProgramStore, - store: AsyncProgramStore, message_needs: Vec, ) { - let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application; + let ChatStore { presences, rooms, previews, settings, worker, .. } = &mut locked.application; let info = rooms.get_or_default(room_id.clone()); info.fetching = false; - let client = &worker.client; match res { Ok((fetch_id, msgs)) => { @@ -345,14 +346,7 @@ fn load_insert( info.insert_encrypted(msg); }, AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(msg)) => { - info.insert_with_preview( - room_id.clone(), - store.clone(), - picker.clone(), - msg, - settings, - client.media(), - ); + info.insert_with_preview(msg, settings, previews, worker); }, AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::Reaction(ev)) => { info.insert_reaction(ev); @@ -637,6 +631,7 @@ pub enum WorkerTask { TypingNotice(OwnedRoomId), Verify(VerifyAction, SasVerification, ClientReply>), VerifyRequest(OwnedUserId, ClientReply>), + LoadImage(MediaSource, ImagePreviewSize, Arc, Arc), } impl Debug for WorkerTask { @@ -700,6 +695,14 @@ impl Debug for WorkerTask { .field(&format_args!("_")) .finish() }, + WorkerTask::LoadImage(source, size, _, _) => { + f.debug_tuple("WorkerTask::RenderImage") + .field(source) + .field(size) + .field(&format_args!("_")) + .field(&format_args!("_")) + .finish() + }, } } } @@ -845,6 +848,16 @@ impl Requester { return response.recv(); } + + pub fn load_image( + &self, + source: MediaSource, + size: ImagePreviewSize, + picker: Arc, + permits: Arc, + ) { + self.tx.send(WorkerTask::LoadImage(source, size, picker, permits)).unwrap(); + } } pub struct ClientWorker { @@ -853,6 +866,9 @@ pub struct ClientWorker { client: Client, load_handle: Option>, sync_handle: Option>, + + /// Take care when locking since worker commands are sent with the lock already hold + store: Option, } impl ClientWorker { @@ -865,6 +881,7 @@ impl ClientWorker { client: client.clone(), load_handle: None, sync_handle: None, + store: None, }; tokio::spawn(async move { @@ -938,6 +955,17 @@ impl ClientWorker { assert!(self.initialized); reply.send(self.verify_request(user_id).await); }, + WorkerTask::LoadImage(source, size, picker, permits) => { + assert!(self.initialized); + tokio::spawn(load_image( + self.store.clone().unwrap(), + self.client.media(), + source, + picker, + permits, + size, + )); + }, } } @@ -1012,20 +1040,14 @@ impl ClientWorker { let sender = ev.sender().to_owned(); let _ = locked.application.presences.get_or_default(sender); - let ChatStore { rooms, picker, settings, .. } = &mut locked.application; + let ChatStore { rooms, previews, settings, worker, .. } = + &mut locked.application; let info = rooms.get_or_default(room_id.to_owned()); update_event_receipts(info, &room, ev.event_id()).await; let full_ev = ev.into_full_event(room_id.to_owned()); - info.insert_with_preview( - room_id.to_owned(), - store.clone(), - picker.clone(), - full_ev, - settings, - client.media(), - ); + info.insert_with_preview(full_ev, settings, previews, worker); } }, ); @@ -1255,6 +1277,8 @@ impl ClientWorker { }, ); + self.store = Some(store.clone()); + self.load_handle = tokio::spawn({ let client = self.client.clone(); let settings = self.settings.clone();