commit fd7238bc4c072a8e023a0433c43e94a56a9211df
parent 9e4c9205699770855bbffac04e7331a62248e5d4
Author: William Casarin <jb55@jb55.com>
Date: Thu, 4 Dec 2025 00:39:48 -0800
Merge large job pool refactor by kernel #1210
kernelkind (38):
feat(blur): add BlurCache
feat(cargo): add crossbeam
feat(gif): add AnimatedImgTexCache
feat(gif): new Animation
feat(gif): new ProcessedGifFrame
feat(hyper): install crypto provider
feat(images): new TextureState
feat(img-parsing): new parse_img_response
feat(job-cache): new JobCache
feat(jobs): add `schedule_receivable`
feat(jobs): mpsc -> mpmc
feat(jobs): structs for new JobCache
feat(jobs-media): basic media jobs structs
feat(media-job): pre & post actions
feat(media-jobs): add media JobCache to app
feat(media-loading): Images helpers
feat(media-loading): add TexturesCache to Images
feat(media-loading): new Images::latest_texture
feat(media-loading): new TexturesCache
feat(media-loading): wire new media loading cache
feat(media-rendering): add helpers for various media rendering configs
feat(network): add hyper
feat(network): hyper networking
feat(static-imgs): add StaticImgTexCache
refactor(gif): nevernest process_gif_frame
refactor(jobs): move related jobs things to own module
refactor(search): remove unnecessary params
refactor: appease clippy
refactor: remove unnecessary old JobsCache code
tmp(refactor): Animation -> AnimationOld
tmp(refactor): Images::latest_texture -> latest_texture_old
tmp(refactor): MediaRenderState -> MediaRenderStateOld
tmp(refactor): ObfuscatedTexture -> ObfuscatedTextureOld
tmp(refactor): ProcessedGifFrame -> ProcessedGifFrameOld
tmp(refactor): TextureState -> TextureStateOld
tmp(refactor): TexturesCache -> TexturesCacheOld
tmp(refactor): parse_img_response -> parse_img_response_old
tmp(refactor): rename current jobs stuff to Old
Diffstat:
59 files changed, 2129 insertions(+), 1580 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -519,6 +519,29 @@ dependencies = [
]
[[package]]
+name = "aws-lc-rs"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc"
+dependencies = [
+ "bindgen 0.72.1",
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
+[[package]]
name = "backoff"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -620,6 +643,26 @@ dependencies = [
]
[[package]]
+name = "bindgen"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+dependencies = [
+ "bitflags 2.9.1",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.12.1",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 2.1.1",
+ "shlex",
+ "syn 2.0.104",
+]
+
+[[package]]
name = "bip39"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1052,6 +1095,15 @@ dependencies = [
]
[[package]]
+name = "cmake"
+version = "0.1.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
+dependencies = [
+ "cc",
+]
+
+[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1155,6 +1207,19 @@ dependencies = [
]
[[package]]
+name = "crossbeam"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
+[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1183,6 +1248,15 @@ dependencies = [
]
[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1412,6 +1486,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2007,6 +2087,12 @@ dependencies = [
]
[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2338,6 +2424,25 @@ dependencies = [
]
[[package]]
+name = "h2"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.9.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
name = "half"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2488,6 +2593,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
name = "human_format"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2495,18 +2606,22 @@ checksum = "5c3b1f728c459d27b12448862017b96ad4767b1ec2ec5e6434e99f1577f085b8"
[[package]]
name = "hyper"
-version = "1.6.0"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
+ "atomic-waker",
"bytes",
"futures-channel",
- "futures-util",
+ "futures-core",
+ "h2",
"http",
"http-body",
"httparse",
+ "httpdate",
"itoa",
"pin-project-lite",
+ "pin-utils",
"smallvec",
"tokio",
"want",
@@ -2521,6 +2636,7 @@ dependencies = [
"http",
"hyper",
"hyper-util",
+ "log",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
@@ -3522,7 +3638,7 @@ name = "nostrdb"
version = "0.8.0"
source = "git+https://github.com/damus-io/nostrdb-rs?rev=6956b9f955463404b8eff3b7abe0cc3092cb5958#6956b9f955463404b8eff3b7abe0cc3092cb5958"
dependencies = [
- "bindgen",
+ "bindgen 0.69.5",
"cc",
"flatbuffers",
"futures",
@@ -3543,6 +3659,7 @@ dependencies = [
"bitflags 2.9.1",
"blurhash",
"chrono",
+ "crossbeam",
"crossbeam-channel",
"dirs",
"eframe",
@@ -3556,6 +3673,10 @@ dependencies = [
"fluent-resmgr",
"hashbrown 0.15.4",
"hex",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
"image",
"indexmap 2.9.0",
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3573,6 +3694,7 @@ dependencies = [
"puffin_egui",
"rand 0.9.2",
"regex",
+ "rustls",
"secp256k1 0.30.0",
"serde",
"serde_json",
@@ -5200,6 +5322,7 @@ version = "0.23.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
dependencies = [
+ "aws-lc-rs",
"log",
"once_cell",
"ring",
@@ -5237,6 +5360,7 @@ version = "0.103.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
dependencies = [
+ "aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@@ -6284,7 +6408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "319c70195101a93f56db4c74733e272d720768e13471f400c78406a326b172b0"
dependencies = [
"cc",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
@@ -33,6 +33,11 @@ egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b
#egui_virtual_list = "0.6.0"
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
ehttp = "0.5.0"
+hyper = { version = "1.7.0", features = ["full"] }
+hyper-util = {version = "0.1" , features = ["tokio"]}
+hyper-rustls = "0.27.7"
+http-body-util = "0.1.3"
+rustls = "0.23.28"
enostr = { path = "crates/enostr" }
ewebsock = { version = "0.2.0", features = ["tls"] }
fluent = "0.17.0"
diff --git a/crates/notedeck/Cargo.toml b/crates/notedeck/Cargo.toml
@@ -53,6 +53,12 @@ chrono = { workspace = true }
indexmap = {workspace = true}
rand = {workspace = true}
crossbeam-channel = "0.5"
+crossbeam = "0.8.4"
+hyper = { workspace = true }
+hyper-util = { workspace = true }
+http-body-util = { workspace = true }
+hyper-rustls = { workspace = true }
+rustls = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }
diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs
@@ -3,13 +3,13 @@ use crate::i18n::Localization;
use crate::persist::{AppSizeHandler, SettingsHandler};
use crate::wallet::GlobalWallet;
use crate::zaps::Zaps;
-use crate::Error;
-use crate::JobPool;
use crate::NotedeckOptions;
use crate::{
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
};
+use crate::{Error, JobCache};
+use crate::{JobPool, MediaJobs};
use egui::Margin;
use egui::ThemePreference;
use egui_winit::clipboard::Clipboard;
@@ -77,6 +77,7 @@ pub struct Notedeck {
zaps: Zaps,
frame_history: FrameHistory,
job_pool: JobPool,
+ media_jobs: MediaJobs,
i18n: Localization,
#[cfg(target_os = "android")]
@@ -122,6 +123,13 @@ impl eframe::App for Notedeck {
self.frame_history
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
+ self.media_jobs.run_received(&mut self.job_pool, |id| {
+ crate::run_media_job_pre_action(id, &mut self.img_cache.textures);
+ });
+ self.media_jobs.deliver_all_completed(|completed| {
+ crate::deliver_completed_media_job(completed, &mut self.img_cache.textures)
+ });
+
// handle account updates
self.accounts.update(&mut self.ndb, &mut self.pool, ctx);
@@ -177,6 +185,8 @@ impl Notedeck {
#[cfg(feature = "puffin")]
setup_puffin();
+ install_crypto();
+
// Skip the first argument, which is the program name.
let (parsed_args, unrecognized_args) = Args::parse(&args[1..]);
@@ -288,6 +298,9 @@ impl Notedeck {
}
}
+ let (send_new_jobs, receive_new_jobs) = std::sync::mpsc::channel();
+ let media_job_cache = JobCache::new(receive_new_jobs, send_new_jobs);
+
Self {
ndb,
img_cache,
@@ -306,6 +319,7 @@ impl Notedeck {
clipboard: Clipboard::new(None),
zaps,
job_pool,
+ media_jobs: media_job_cache,
i18n,
#[cfg(target_os = "android")]
android_app: None,
@@ -371,6 +385,7 @@ impl Notedeck {
zaps: &mut self.zaps,
frame_history: &mut self.frame_history,
job_pool: &mut self.job_pool,
+ media_jobs: &mut self.media_jobs,
i18n: &mut self.i18n,
#[cfg(target_os = "android")]
android: self.android_app.as_ref().unwrap().clone(),
@@ -401,3 +416,8 @@ impl Notedeck {
&self.unrecognized_args
}
}
+
+pub fn install_crypto() {
+ let provider = rustls::crypto::aws_lc_rs::default_provider();
+ let _ = provider.install_default();
+}
diff --git a/crates/notedeck/src/context.rs b/crates/notedeck/src/context.rs
@@ -1,7 +1,7 @@
use crate::{
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
- wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, SettingsHandler,
- UnknownIds,
+ wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, MediaJobs, NoteCache,
+ SettingsHandler, UnknownIds,
};
use egui_winit::clipboard::Clipboard;
@@ -28,6 +28,7 @@ pub struct AppContext<'a> {
pub zaps: &'a mut Zaps,
pub frame_history: &'a mut FrameHistory,
pub job_pool: &'a mut JobPool,
+ pub media_jobs: &'a mut MediaJobs,
pub i18n: &'a mut Localization,
#[cfg(target_os = "android")]
diff --git a/crates/notedeck/src/imgcache.rs b/crates/notedeck/src/imgcache.rs
@@ -1,6 +1,10 @@
-use crate::media::gif::ensure_latest_texture_from_cache;
+use crate::jobs::MediaJobSender;
+use crate::media::gif::AnimatedImgTexCache;
use crate::media::images::ImageType;
-use crate::media::AnimationMode;
+use crate::media::static_imgs::StaticImgTexCache;
+use crate::media::{
+ AnimationMode, BlurCache, NoLoadingLatestTex, TrustedMediaLatestTex, UntrustedMediaLatestTex,
+};
use crate::urls::{UrlCache, UrlMimes};
use crate::ImageMetadata;
use crate::ObfuscationType;
@@ -8,13 +12,11 @@ use crate::RenderableMedia;
use crate::Result;
use egui::TextureHandle;
use image::{Delay, Frame};
-use poll_promise::Promise;
use egui::ColorImage;
use std::collections::HashMap;
use std::fs::{self, create_dir_all, File};
-use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime};
use std::{io, thread};
@@ -25,177 +27,38 @@ use std::path::PathBuf;
use std::path::{self, Path};
use tracing::warn;
-#[derive(Default)]
pub struct TexturesCache {
- pub cache: hashbrown::HashMap<String, TextureStateInternal>,
+ pub static_image: StaticImgTexCache,
+ pub blurred: BlurCache,
+ pub animated: AnimatedImgTexCache,
}
impl TexturesCache {
- pub fn handle_and_get_or_insert_loadable(
- &mut self,
- url: &str,
- closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
- ) -> LoadableTextureState<'_> {
- let internal = self.handle_and_get_state_internal(url, true, closure);
-
- internal.into()
- }
-
- pub fn handle_and_get_or_insert(
- &mut self,
- url: &str,
- closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
- ) -> TextureState<'_> {
- let internal = self.handle_and_get_state_internal(url, false, closure);
-
- internal.into()
- }
-
- fn handle_and_get_state_internal(
- &mut self,
- url: &str,
- use_loading: bool,
- closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
- ) -> &mut TextureStateInternal {
- let state = match self.cache.raw_entry_mut().from_key(url) {
- hashbrown::hash_map::RawEntryMut::Occupied(entry) => {
- let state = entry.into_mut();
- handle_occupied(state, use_loading);
-
- state
- }
- hashbrown::hash_map::RawEntryMut::Vacant(entry) => {
- let res = closure();
- let (_, state) = entry.insert(url.to_owned(), TextureStateInternal::Pending(res));
-
- state
- }
- };
-
- state
- }
-
- pub fn insert_pending(&mut self, url: &str, promise: Promise<Option<Result<TexturedImage>>>) {
- self.cache
- .insert(url.to_owned(), TextureStateInternal::Pending(promise));
- }
-
- pub fn move_to_loaded(&mut self, url: &str) {
- let hashbrown::hash_map::RawEntryMut::Occupied(entry) =
- self.cache.raw_entry_mut().from_key(url)
- else {
- return;
- };
-
- entry.replace_entry_with(|_, v| {
- let TextureStateInternal::Loading(textured) = v else {
- return Some(v);
- };
-
- Some(TextureStateInternal::Loaded(textured))
- });
- }
-
- pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState<'_>> {
- self.cache.get_mut(url).map(|state| {
- handle_occupied(state, true);
- state.into()
- })
- }
-}
-
-fn handle_occupied(state: &mut TextureStateInternal, use_loading: bool) {
- let TextureStateInternal::Pending(promise) = state else {
- return;
- };
-
- let Some(res) = promise.ready_mut() else {
- return;
- };
-
- let Some(res) = res.take() else {
- tracing::error!("Failed to take the promise");
- *state =
- TextureStateInternal::Error(crate::Error::Generic("Promise already taken".to_owned()));
- return;
- };
-
- match res {
- Ok(textured) => {
- *state = if use_loading {
- TextureStateInternal::Loading(textured)
- } else {
- TextureStateInternal::Loaded(textured)
- }
+ pub fn new(base_dir: PathBuf) -> Self {
+ Self {
+ static_image: StaticImgTexCache::new(
+ base_dir.join(MediaCache::rel_dir(MediaCacheType::Image)),
+ ),
+ blurred: Default::default(),
+ animated: AnimatedImgTexCache::new(
+ base_dir.join(MediaCache::rel_dir(MediaCacheType::Gif)),
+ ),
}
- Err(e) => *state = TextureStateInternal::Error(e),
}
}
-pub enum LoadableTextureState<'a> {
+pub enum TextureState<T> {
Pending,
- Error(&'a crate::Error),
- Loading {
- actual_image_tex: &'a mut TexturedImage,
- }, // the texture is in the loading state, for transitioning between the pending and loaded states
- Loaded(&'a mut TexturedImage),
-}
-
-pub enum TextureState<'a> {
- Pending,
- Error(&'a crate::Error),
- Loaded(&'a mut TexturedImage),
-}
-
-impl<'a> TextureState<'a> {
- pub fn is_loaded(&self) -> bool {
- matches!(self, Self::Loaded(_))
- }
-}
-
-impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> {
- fn from(value: &'a mut TextureStateInternal) -> Self {
- match value {
- TextureStateInternal::Pending(_) => TextureState::Pending,
- TextureStateInternal::Error(error) => TextureState::Error(error),
- TextureStateInternal::Loading(textured_image) => TextureState::Loaded(textured_image),
- TextureStateInternal::Loaded(textured_image) => TextureState::Loaded(textured_image),
- }
- }
-}
-
-pub enum TextureStateInternal {
- Pending(Promise<Option<Result<TexturedImage>>>),
Error(crate::Error),
- Loading(TexturedImage), // the image is in the loading state, for transitioning between blur and image
- Loaded(TexturedImage),
+ Loaded(T),
}
-impl<'a> From<&'a mut TextureStateInternal> for LoadableTextureState<'a> {
- fn from(value: &'a mut TextureStateInternal) -> Self {
- match value {
- TextureStateInternal::Pending(_) => LoadableTextureState::Pending,
- TextureStateInternal::Error(error) => LoadableTextureState::Error(error),
- TextureStateInternal::Loading(textured_image) => LoadableTextureState::Loading {
- actual_image_tex: textured_image,
- },
- TextureStateInternal::Loaded(textured_image) => {
- LoadableTextureState::Loaded(textured_image)
- }
- }
- }
-}
-
-pub enum TexturedImage {
- Static(TextureHandle),
- Animated(Animation),
-}
-
-impl TexturedImage {
- pub fn get_first_texture(&self) -> &TextureHandle {
+impl<T> std::fmt::Debug for TextureState<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
- TexturedImage::Static(texture_handle) => texture_handle,
- TexturedImage::Animated(animation) => &animation.first_frame.texture,
+ Self::Pending => write!(f, "Pending"),
+ Self::Error(_) => f.debug_tuple("Error").field(&"").finish(),
+ Self::Loaded(_) => f.debug_tuple("Loaded").field(&"").finish(),
}
}
}
@@ -203,7 +66,6 @@ impl TexturedImage {
pub struct Animation {
pub first_frame: TextureFrame,
pub other_frames: Vec<TextureFrame>,
- pub receiver: Option<Receiver<TextureFrame>>,
}
impl Animation {
@@ -232,7 +94,6 @@ pub struct ImageFrame {
pub struct MediaCache {
pub cache_dir: path::PathBuf,
- pub textures_cache: TexturesCache,
pub cache_type: MediaCacheType,
pub cache_size: Arc<Mutex<Option<u64>>>,
}
@@ -266,7 +127,6 @@ impl MediaCache {
Self {
cache_dir,
- textures_cache: TexturesCache::default(),
cache_type,
cache_size,
}
@@ -370,7 +230,6 @@ impl MediaCache {
}
fn clear(&mut self) {
- self.textures_cache.cache.clear();
*self.cache_size.try_lock().unwrap() = Some(0);
}
}
@@ -413,6 +272,7 @@ pub struct Images {
pub base_path: path::PathBuf,
pub static_imgs: MediaCache,
pub gifs: MediaCache,
+ pub textures: TexturesCache,
pub urls: UrlMimes,
/// cached imeta data
pub metadata: HashMap<String, ImageMetadata>,
@@ -429,6 +289,7 @@ impl Images {
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
gif_states: Default::default(),
metadata: Default::default(),
+ textures: TexturesCache::new(path.clone()),
}
}
@@ -460,40 +321,22 @@ impl Images {
})
}
- pub fn latest_texture(
- &mut self,
+ pub fn latest_texture<'a>(
+ &'a mut self,
+ jobs: &MediaJobSender,
ui: &mut egui::Ui,
url: &str,
img_type: ImageType,
animation_mode: AnimationMode,
- ) -> Option<TextureHandle> {
+ ) -> Option<&'a TextureHandle> {
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
- let cache_dir = self.get_cache(cache_type).cache_dir.clone();
- let is_loaded = self
- .get_cache_mut(cache_type)
- .textures_cache
- .handle_and_get_or_insert(url, || {
- crate::media::images::fetch_img(&cache_dir, ui.ctx(), url, img_type, cache_type)
- })
- .is_loaded();
-
- if !is_loaded {
- return None;
- }
-
- let cache = match cache_type {
- MediaCacheType::Image => &mut self.static_imgs,
- MediaCacheType::Gif => &mut self.gifs,
- };
-
- ensure_latest_texture_from_cache(
- ui,
- url,
+ let mut loader = NoLoadingLatestTex::new(
+ &self.textures.static_image,
+ &self.textures.animated,
&mut self.gif_states,
- &mut cache.textures_cache,
- animation_mode,
- )
+ );
+ loader.latest(jobs, ui.ctx(), url, cache_type, img_type, animation_mode)
}
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
@@ -529,6 +372,36 @@ impl Images {
Ok(())
}
+
+ pub fn trusted_texture_loader(&mut self) -> TrustedMediaLatestTex<'_> {
+ TrustedMediaLatestTex::new(
+ NoLoadingLatestTex::new(
+ &self.textures.static_image,
+ &self.textures.animated,
+ &mut self.gif_states,
+ ),
+ &self.textures.blurred,
+ )
+ }
+
+ pub fn untrusted_texture_loader(&mut self) -> UntrustedMediaLatestTex<'_> {
+ UntrustedMediaLatestTex::new(&self.textures.blurred)
+ }
+
+ pub fn no_img_loading_tex_loader(&'_ mut self) -> NoLoadingLatestTex<'_> {
+ NoLoadingLatestTex::new(
+ &self.textures.static_image,
+ &self.textures.animated,
+ &mut self.gif_states,
+ )
+ }
+
+ pub fn user_trusts_img(&self, url: &str, media_type: MediaCacheType) -> bool {
+ match media_type {
+ MediaCacheType::Image => self.textures.static_image.contains(url),
+ MediaCacheType::Gif => self.textures.animated.contains(url),
+ }
+ }
}
pub type GifStateMap = HashMap<String, GifState>;
@@ -544,31 +417,3 @@ pub struct LatestTexture {
pub texture: TextureHandle,
pub request_next_repaint: Option<SystemTime>,
}
-
-#[profiling::function]
-pub fn get_render_state<'a>(
- ctx: &egui::Context,
- images: &'a mut Images,
- cache_type: MediaCacheType,
- url: &str,
- img_type: ImageType,
-) -> RenderState<'a> {
- let cache = match cache_type {
- MediaCacheType::Image => &mut images.static_imgs,
- MediaCacheType::Gif => &mut images.gifs,
- };
-
- let texture_state = cache.textures_cache.handle_and_get_or_insert(url, || {
- crate::media::images::fetch_img(&cache.cache_dir, ctx, url, img_type, cache_type)
- });
-
- RenderState {
- texture_state,
- gifs: &mut images.gif_states,
- }
-}
-
-pub struct RenderState<'a> {
- pub texture_state: TextureState<'a>,
- pub gifs: &'a mut GifStateMap,
-}
diff --git a/crates/notedeck/src/job_pool.rs b/crates/notedeck/src/job_pool.rs
@@ -1,100 +0,0 @@
-use std::{
- future::Future,
- sync::{
- mpsc::{self, Sender},
- Arc, Mutex,
- },
-};
-use tokio::sync::oneshot;
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-pub struct JobPool {
- tx: Sender<Job>,
-}
-
-impl Default for JobPool {
- fn default() -> Self {
- JobPool::new(2)
- }
-}
-
-impl JobPool {
- pub fn new(num_threads: usize) -> Self {
- let (tx, rx) = mpsc::channel::<Job>();
-
- // TODO(jb55) why not mpmc here !???
- let arc_rx = Arc::new(Mutex::new(rx));
- for _ in 0..num_threads {
- let arc_rx_clone = arc_rx.clone();
- std::thread::spawn(move || loop {
- let job = {
- let Ok(unlocked) = arc_rx_clone.lock() else {
- continue;
- };
- let Ok(job) = unlocked.recv() else {
- continue;
- };
-
- job
- };
-
- job();
- });
- }
-
- Self { tx }
- }
-
- pub fn schedule<F, T>(&self, job: F) -> impl Future<Output = T>
- where
- F: FnOnce() -> T + Send + 'static,
- T: Send + 'static,
- {
- let (tx_result, rx_result) = oneshot::channel::<T>();
-
- let job = Box::new(move || {
- let output = job();
- let _ = tx_result.send(output);
- });
-
- self.tx
- .send(job)
- .expect("receiver should not be deallocated");
-
- async move {
- rx_result.await.unwrap_or_else(|_| {
- panic!("Worker thread or channel dropped before returning the result.")
- })
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::job_pool::JobPool;
-
- fn test_fn(a: u32, b: u32) -> u32 {
- a + b
- }
-
- #[tokio::test]
- async fn test() {
- let pool = JobPool::default();
-
- // Now each job can return different T
- let future_str = pool.schedule(|| -> String { "hello from string job".into() });
-
- let a = 5;
- let b = 6;
- let future_int = pool.schedule(move || -> u32 { test_fn(a, b) });
-
- println!("(Meanwhile we can do more async work) ...");
-
- let s = future_str.await;
- let i = future_int.await;
-
- println!("Got string: {:?}", s);
- println!("Got integer: {}", i);
- }
-}
diff --git a/crates/notedeck/src/jobs.rs b/crates/notedeck/src/jobs.rs
@@ -1,153 +0,0 @@
-use crate::JobPool;
-use egui::TextureHandle;
-use hashbrown::{hash_map::RawEntryMut, HashMap};
-use poll_promise::Promise;
-
-#[derive(Default)]
-pub struct JobsCache {
- jobs: HashMap<JobIdOwned, JobState>,
-}
-
-pub enum JobState {
- Pending(Promise<Option<Result<Job, JobError>>>),
- Error(JobError),
- Completed(Job),
-}
-
-pub enum JobError {
- InvalidParameters,
-}
-
-#[derive(Debug)]
-pub enum JobParams<'a> {
- Blurhash(BlurhashParams<'a>),
-}
-
-#[derive(Debug)]
-pub enum JobParamsOwned {
- Blurhash(BlurhashParamsOwned),
-}
-
-impl<'a> From<BlurhashParams<'a>> for BlurhashParamsOwned {
- fn from(params: BlurhashParams<'a>) -> Self {
- BlurhashParamsOwned {
- blurhash: params.blurhash.to_owned(),
- url: params.url.to_owned(),
- ctx: params.ctx.clone(),
- }
- }
-}
-
-impl<'a> From<JobParams<'a>> for JobParamsOwned {
- fn from(params: JobParams<'a>) -> Self {
- match params {
- JobParams::Blurhash(bp) => JobParamsOwned::Blurhash(bp.into()),
- }
- }
-}
-
-#[derive(Debug)]
-pub struct BlurhashParams<'a> {
- pub blurhash: &'a str,
- pub url: &'a str,
- pub ctx: &'a egui::Context,
-}
-
-#[derive(Debug)]
-pub struct BlurhashParamsOwned {
- pub blurhash: String,
- pub url: String,
- pub ctx: egui::Context,
-}
-
-impl JobsCache {
- pub fn get_or_insert_with<
- 'a,
- F: FnOnce(Option<JobParamsOwned>) -> Result<Job, JobError> + Send + 'static,
- >(
- &'a mut self,
- job_pool: &mut JobPool,
- jobid: &JobId,
- params: Option<JobParams>,
- run_job: F,
- ) -> &'a mut JobState {
- match self.jobs.raw_entry_mut().from_key(jobid) {
- RawEntryMut::Occupied(entry) => 's: {
- let mut state = entry.into_mut();
-
- let JobState::Pending(promise) = &mut state else {
- break 's state;
- };
-
- let Some(res) = promise.ready_mut() else {
- break 's state;
- };
-
- let Some(res) = res.take() else {
- tracing::error!("Failed to take the promise for job: {:?}", jobid);
- break 's state;
- };
-
- *state = match res {
- Ok(j) => JobState::Completed(j),
- Err(e) => JobState::Error(e),
- };
-
- state
- }
- RawEntryMut::Vacant(entry) => {
- let owned_params = params.map(JobParams::into);
- let wrapped: Box<dyn FnOnce() -> Option<Result<Job, JobError>> + Send + 'static> =
- Box::new(move || Some(run_job(owned_params)));
-
- let promise = Promise::spawn_async(job_pool.schedule(wrapped));
-
- let (_, state) = entry.insert(jobid.into(), JobState::Pending(promise));
-
- state
- }
- }
- }
-
- pub fn get(&self, jobid: &JobId) -> Option<&JobState> {
- self.jobs.get(jobid)
- }
-}
-
-impl<'a> From<&JobId<'a>> for JobIdOwned {
- fn from(jobid: &JobId<'a>) -> Self {
- match jobid {
- JobId::Blurhash(s) => JobIdOwned::Blurhash(s.to_string()),
- }
- }
-}
-
-impl hashbrown::Equivalent<JobIdOwned> for JobId<'_> {
- fn equivalent(&self, key: &JobIdOwned) -> bool {
- match (self, key) {
- (JobId::Blurhash(a), JobIdOwned::Blurhash(b)) => *a == b.as_str(),
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Hash)]
-enum JobIdOwned {
- Blurhash(String), // image URL
-}
-
-#[derive(Debug, Hash)]
-pub enum JobId<'a> {
- Blurhash(&'a str), // image URL
-}
-
-pub enum Job {
- Blurhash(Option<TextureHandle>),
-}
-
-impl std::fmt::Debug for Job {
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- match self {
- Job::Blurhash(_) => write!(f, "Blurhash"),
- }
- }
-}
diff --git a/crates/notedeck/src/jobs/cache.rs b/crates/notedeck/src/jobs/cache.rs
@@ -0,0 +1,201 @@
+use std::{
+ collections::HashSet,
+ fmt::Debug,
+ hash::Hash,
+ sync::mpsc::{Receiver, Sender},
+};
+
+use crossbeam::queue::SegQueue;
+
+use crate::jobs::types::{
+ AsyncJob, JobAccess, JobComplete, JobId, JobIdAccessible, JobOutput, JobPackage, JobRun,
+ NoOutputRun, RunType,
+};
+use crate::jobs::JobPool;
+
+type CompletionQueue<K, T> = std::sync::Arc<SegQueue<JobComplete<K, T>>>;
+
+pub struct JobCache<K, T: 'static> {
+ receive_new_jobs: Receiver<JobPackage<K, T>>,
+ running: HashSet<JobId<K>>,
+ send_new_jobs: Sender<JobPackage<K, T>>,
+ completed: CompletionQueue<K, T>,
+}
+
+impl<K, T> JobCache<K, T>
+where
+ K: Hash + Eq + Clone + Debug + Send + 'static,
+ T: Send + 'static,
+{
+ pub fn new(
+ receive_new_jobs: Receiver<JobPackage<K, T>>,
+ send_new_jobs: Sender<JobPackage<K, T>>,
+ ) -> Self {
+ Self {
+ receive_new_jobs,
+ send_new_jobs,
+ completed: Default::default(),
+ running: Default::default(),
+ }
+ }
+
+ pub fn run_received(&mut self, pool: &mut JobPool, mut pre_action: impl FnMut(&JobId<K>)) {
+ for pkg in self.receive_new_jobs.try_iter() {
+ let id = &pkg.id;
+ if JobAccess::Public == id.access && self.running.contains(&id.job_id) {
+ tracing::warn!("Ignoring request to run {id:?} since it's already running");
+ continue;
+ }
+ self.running.insert(id.job_id.clone());
+
+ let job_run = match pkg.run {
+ RunType::NoOutput(run) => {
+ no_output_run(pool, run);
+ continue;
+ }
+ RunType::Output(job_run) => job_run,
+ };
+
+ pre_action(&id.job_id);
+
+ run_received_job(
+ job_run,
+ pool,
+ self.send_new_jobs.clone(),
+ self.completed.clone(),
+ pkg.id,
+ );
+ }
+ }
+
+ pub fn deliver_all_completed(&mut self, mut deliver_complete: impl FnMut(JobComplete<K, T>)) {
+ while let Some(res) = self.completed.pop() {
+ tracing::trace!("Got completed: {:?}", res.job_id);
+ let id = res.job_id.clone();
+ deliver_complete(res);
+ self.running.remove(&id);
+ }
+ }
+
+ pub fn sender(&self) -> &Sender<JobPackage<K, T>> {
+ &self.send_new_jobs
+ }
+}
+
+fn run_received_job<K, T>(
+ job_run: JobRun<T>,
+ pool: &mut JobPool,
+ send_new_jobs: Sender<JobPackage<K, T>>,
+ completion_queue: CompletionQueue<K, T>,
+ id: JobIdAccessible<K>,
+) where
+ K: Hash + Eq + Clone + Debug + Send + 'static,
+ T: Send + 'static,
+{
+ match job_run {
+ JobRun::Sync(run) => {
+ run_sync(pool, send_new_jobs, completion_queue, id, run);
+ }
+ JobRun::Async(run) => {
+ run_async(send_new_jobs, completion_queue, id, run);
+ }
+ }
+}
+
+fn run_sync<F, K, T>(
+ job_pool: &mut JobPool,
+ send_new_jobs: Sender<JobPackage<K, T>>,
+ completion_queue: CompletionQueue<K, T>,
+ id: JobIdAccessible<K>,
+ run_job: F,
+) where
+ F: FnOnce() -> JobOutput<T> + Send + 'static,
+ K: Hash + Eq + Clone + Debug + Send + 'static,
+ T: Send + 'static,
+{
+ let id_c = id.clone();
+ let wrapped: Box<dyn FnOnce() + Send + 'static> = Box::new(move || {
+ let res = run_job();
+ match res {
+ JobOutput::Complete(complete_response) => {
+ completion_queue.push(JobComplete {
+ job_id: id.job_id.clone(),
+ response: complete_response.response,
+ });
+ if let Some(run) = complete_response.run_no_output {
+ if let Err(e) = send_new_jobs.send(JobPackage {
+ id: id.into_internal(),
+ run: RunType::NoOutput(run),
+ }) {
+ tracing::error!("{e}");
+ }
+ }
+ }
+ JobOutput::Next(job_run) => {
+ if let Err(e) = send_new_jobs.send(JobPackage {
+ id: id.into_internal(),
+ run: RunType::Output(job_run),
+ }) {
+ tracing::error!("{e}");
+ }
+ }
+ }
+ });
+
+ tracing::trace!("Spawning sync job: {id_c:?}");
+ job_pool.schedule_no_output(wrapped);
+}
+
+fn run_async<K, T>(
+ send_new_jobs: Sender<JobPackage<K, T>>,
+ completion_queue: CompletionQueue<K, T>,
+ id: JobIdAccessible<K>,
+ run_job: AsyncJob<T>,
+) where
+ K: Hash + Eq + Clone + Debug + Send + 'static,
+ T: Send + 'static,
+{
+ tracing::trace!("Spawning async job: {id:?}");
+ tokio::spawn(async move {
+ {
+ let res = run_job.await;
+ match res {
+ JobOutput::Complete(complete_response) => {
+ completion_queue.push(JobComplete {
+ job_id: id.job_id.clone(),
+ response: complete_response.response,
+ });
+ if let Some(run) = complete_response.run_no_output {
+ if let Err(e) = send_new_jobs.send(JobPackage {
+ id: id.into_internal(),
+ run: RunType::NoOutput(run),
+ }) {
+ tracing::error!("{e}");
+ }
+ }
+ }
+ JobOutput::Next(job_run) => {
+ if let Err(e) = send_new_jobs.send(JobPackage {
+ id: id.into_internal(),
+ run: RunType::Output(job_run),
+ }) {
+ tracing::error!("{e}");
+ }
+ }
+ }
+ }
+ });
+}
+
+fn no_output_run(pool: &mut JobPool, run: NoOutputRun) {
+ match run {
+ NoOutputRun::Sync(c) => {
+ tracing::trace!("Spawning no output sync job");
+ pool.schedule_no_output(c);
+ }
+ NoOutputRun::Async(f) => {
+ tracing::trace!("Spawning no output async sync job");
+ tokio::spawn(f);
+ }
+ }
+}
diff --git a/crates/notedeck/src/jobs/job_pool.rs b/crates/notedeck/src/jobs/job_pool.rs
@@ -0,0 +1,102 @@
+use crossbeam::channel;
+use std::future::Future;
+use tokio::sync::oneshot::{self, Receiver};
+
+type Job = Box<dyn FnOnce() + Send + 'static>;
+
+pub struct JobPool {
+ tx: channel::Sender<Job>,
+}
+
+impl Default for JobPool {
+ fn default() -> Self {
+ JobPool::new(2)
+ }
+}
+
+impl JobPool {
+ pub fn new(num_threads: usize) -> Self {
+ let (tx, rx) = channel::unbounded::<Job>();
+ for i in 0..num_threads {
+ let rx = rx.clone();
+ std::thread::spawn(move || {
+ for job in rx.iter() {
+ tracing::trace!("Starting job on thread {i}");
+ job();
+ tracing::trace!("Finished job on thread {i}");
+ }
+ });
+ }
+
+ Self { tx }
+ }
+
+ pub fn schedule<F, T>(&self, job: F) -> impl Future<Output = T>
+ where
+ F: FnOnce() -> T + Send + 'static,
+ T: Send + 'static,
+ {
+ let rx_result = self.schedule_receivable(job);
+ async move {
+ rx_result.await.unwrap_or_else(|_| {
+ panic!("Worker thread or channel dropped before returning the result.")
+ })
+ }
+ }
+
+ pub fn schedule_receivable<F, T>(&self, job: F) -> Receiver<T>
+ where
+ F: FnOnce() -> T + Send + 'static,
+ T: Send + 'static,
+ {
+ let (tx_result, rx_result) = oneshot::channel::<T>();
+
+ let job = Box::new(move || {
+ let output = job();
+ let _ = tx_result.send(output);
+ });
+
+ self.push_job(job);
+
+ rx_result
+ }
+
+ pub fn schedule_no_output(&self, job: impl FnOnce() + Send + 'static) {
+ self.push_job(Box::new(job));
+ }
+
+ fn push_job(&self, job: Job) {
+ if let Err(e) = self.tx.send(job) {
+ tracing::error!("job queue closed unexpectedly: {e}");
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::jobs::JobPool;
+
+ fn test_fn(a: u32, b: u32) -> u32 {
+ a + b
+ }
+
+ #[tokio::test]
+ async fn test() {
+ let pool = JobPool::default();
+
+ // Now each job can return different T
+ let future_str = pool.schedule(|| -> String { "hello from string job".into() });
+
+ let a = 5;
+ let b = 6;
+ let future_int = pool.schedule(move || -> u32 { test_fn(a, b) });
+
+ println!("(Meanwhile we can do more async work) ...");
+
+ let s = future_str.await;
+ let i = future_int.await;
+
+ println!("Got string: {:?}", s);
+ println!("Got integer: {}", i);
+ }
+}
diff --git a/crates/notedeck/src/jobs/media.rs b/crates/notedeck/src/jobs/media.rs
@@ -0,0 +1,78 @@
+use std::sync::mpsc::Sender;
+
+use egui::TextureHandle;
+
+use crate::jobs::JobCache;
+use crate::{Animation, Error, TextureState, TexturesCache};
+
+use crate::jobs::types::{JobComplete, JobId, JobPackage};
+
+pub type MediaJobs = JobCache<MediaJobKind, MediaJobResult>;
+pub type MediaJobSender = Sender<JobPackage<MediaJobKind, MediaJobResult>>;
+
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub enum MediaJobKind {
+ Blurhash,
+ StaticImg,
+ AnimatedImg,
+}
+
+pub enum MediaJobResult {
+ StaticImg(Result<TextureHandle, Error>),
+ Blurhash(Result<TextureHandle, Error>),
+ Animation(Result<Animation, Error>),
+}
+
+pub fn deliver_completed_media_job(
+ completed: JobComplete<MediaJobKind, MediaJobResult>,
+ tex_cache: &mut TexturesCache,
+) {
+ let id = completed.job_id.id;
+ let id_c = id.clone();
+ match completed.response {
+ MediaJobResult::StaticImg(job_complete) => {
+ let r = match job_complete {
+ Ok(t) => TextureState::Loaded(t),
+ Err(e) => TextureState::Error(e),
+ };
+ tex_cache.static_image.cache.insert(id, r);
+ }
+ MediaJobResult::Animation(animation) => {
+ let r = match animation {
+ Ok(a) => TextureState::Loaded(a),
+ Err(e) => TextureState::Error(e),
+ };
+
+ tex_cache.animated.cache.insert(id, r);
+ }
+ MediaJobResult::Blurhash(texture_handle) => {
+ let r = match texture_handle {
+ Ok(t) => TextureState::Loaded(t),
+ Err(e) => TextureState::Error(e),
+ };
+ tex_cache.blurred.cache.insert(id, r.into());
+ }
+ }
+ tracing::trace!("Delivered job for {id_c}");
+}
+
+pub fn run_media_job_pre_action(job_id: &JobId<MediaJobKind>, tex_cache: &mut TexturesCache) {
+ let id = job_id.id.clone();
+ match job_id.job_kind {
+ MediaJobKind::Blurhash => {
+ tex_cache
+ .blurred
+ .cache
+ .insert(id, TextureState::Pending.into());
+ }
+ MediaJobKind::StaticImg => {
+ tex_cache
+ .static_image
+ .cache
+ .insert(id, TextureState::Pending);
+ }
+ MediaJobKind::AnimatedImg => {
+ tex_cache.animated.cache.insert(id, TextureState::Pending);
+ }
+ }
+}
diff --git a/crates/notedeck/src/jobs/mod.rs b/crates/notedeck/src/jobs/mod.rs
@@ -0,0 +1,14 @@
+mod cache;
+mod job_pool;
+mod media;
+pub(crate) mod types;
+
+pub use crate::jobs::types::{
+ CompleteResponse, JobOutput, JobPackage, JobRun, NoOutputRun, RunType,
+};
+pub use cache::JobCache;
+pub use job_pool::JobPool;
+pub use media::{
+ deliver_completed_media_job, run_media_job_pre_action, MediaJobKind, MediaJobResult,
+ MediaJobSender, MediaJobs,
+};
diff --git a/crates/notedeck/src/jobs/types.rs b/crates/notedeck/src/jobs/types.rs
@@ -0,0 +1,100 @@
+use std::{future::Future, pin::Pin};
+
+pub enum JobOutput<T> {
+ Complete(CompleteResponse<T>),
+ Next(JobRun<T>),
+}
+
+impl<T> JobOutput<T> {
+ pub fn complete(response: T) -> Self {
+ JobOutput::Complete(CompleteResponse::new(response))
+ }
+}
+
+pub struct CompleteResponse<T> {
+ pub(crate) response: T,
+ pub(crate) run_no_output: Option<NoOutputRun>,
+}
+
+pub struct JobComplete<K, T> {
+ pub job_id: JobId<K>,
+ pub response: T,
+}
+
+impl<T> CompleteResponse<T> {
+ pub fn new(response: T) -> Self {
+ Self {
+ response,
+ run_no_output: None,
+ }
+ }
+
+ pub fn run_no_output(mut self, run: NoOutputRun) -> Self {
+ self.run_no_output = Some(run);
+ self
+ }
+}
+
+pub enum NoOutputRun {
+ Sync(Box<dyn FnOnce() + Send + 'static>),
+ Async(Pin<Box<dyn Future<Output = ()> + Send + 'static>>),
+}
+
+pub(crate) type SyncJob<T> = Box<dyn FnOnce() -> JobOutput<T> + Send + 'static>;
+pub(crate) type AsyncJob<T> = Pin<Box<dyn Future<Output = JobOutput<T>> + Send + 'static>>;
+
+pub enum JobRun<T> {
+ Sync(SyncJob<T>),
+ Async(AsyncJob<T>),
+}
+
+pub struct JobPackage<K, T> {
+ pub(crate) id: JobIdAccessible<K>,
+ pub(crate) run: RunType<T>,
+}
+
+impl<K, T> JobPackage<K, T> {
+ pub fn new(id: String, job_kind: K, run: RunType<T>) -> Self {
+ Self {
+ id: JobIdAccessible::new_public(id, job_kind),
+ run,
+ }
+ }
+}
+
+pub enum RunType<T> {
+ NoOutput(NoOutputRun),
+ Output(JobRun<T>),
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub struct JobId<K> {
+ pub id: String,
+ pub job_kind: K,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub(crate) enum JobAccess {
+ Public, // Jobs requested outside the cache
+ Internal, // Jobs requested inside the cache
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub(crate) struct JobIdAccessible<K> {
+ pub access: JobAccess,
+ pub job_id: JobId<K>,
+}
+
+impl<K> JobIdAccessible<K> {
+ pub fn new_public(id: String, job_kind: K) -> Self {
+ Self {
+ job_id: JobId { id, job_kind },
+ access: JobAccess::Public,
+ }
+ }
+
+ pub fn into_internal(mut self) -> Self {
+ self.access = JobAccess::Internal;
+ self
+ }
+}
diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs
@@ -13,8 +13,7 @@ pub mod fonts;
mod frame_history;
pub mod i18n;
mod imgcache;
-mod job_pool;
-mod jobs;
+pub mod jobs;
pub mod media;
mod muted;
pub mod name;
@@ -55,17 +54,16 @@ pub use filter::{FilterState, FilterStates, UnifiedSubscription};
pub use fonts::NamedFontFamily;
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
pub use imgcache::{
- get_render_state, Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture,
- LoadableTextureState, MediaCache, MediaCacheType, RenderState, TextureFrame, TextureState,
- TexturedImage, TexturesCache,
+ Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture, MediaCache,
+ MediaCacheType, TextureFrame, TextureState, TexturesCache,
};
-pub use job_pool::JobPool;
pub use jobs::{
- BlurhashParams, Job, JobError, JobId, JobParams, JobParamsOwned, JobState, JobsCache,
+ deliver_completed_media_job, run_media_job_pre_action, JobCache, JobPool, MediaJobSender,
+ MediaJobs,
};
pub use media::{
- compute_blurhash, update_imeta_blurhashes, ImageMetadata, ImageType, MediaAction,
- ObfuscationType, PixelDimensions, PointDimensions, RenderableMedia,
+ update_imeta_blurhashes, ImageMetadata, ImageType, MediaAction, ObfuscationType,
+ PixelDimensions, PointDimensions, RenderableMedia,
};
pub use muted::{MuteFun, Muted};
pub use name::NostrName;
diff --git a/crates/notedeck/src/media/action.rs b/crates/notedeck/src/media/action.rs
@@ -1,5 +1,4 @@
-use crate::{Images, MediaCacheType, TexturedImage};
-use poll_promise::Promise;
+use crate::{jobs::MediaJobSender, ImageType, Images, MediaCacheType};
/// Tracks where media was on the screen so that
/// we can do fun animations when opening the
@@ -41,8 +40,8 @@ pub enum MediaAction {
FetchImage {
url: String,
cache_type: MediaCacheType,
- no_pfp_promise: Promise<Option<Result<TexturedImage, crate::Error>>>,
},
+ // A media is "done loading" when it has the actual media and it has reached the peak of a shimmer, to transition smoothly
DoneLoading {
url: String,
cache_type: MediaCacheType,
@@ -60,15 +59,10 @@ impl std::fmt::Debug for MediaAction {
.field("clicked_index", clicked_index)
.field("media", medias)
.finish(),
- Self::FetchImage {
- url,
- cache_type,
- no_pfp_promise,
- } => f
+ Self::FetchImage { url, cache_type } => f
.debug_struct("FetchNoPfpImage")
.field("url", url)
.field("cache_type", cache_type)
- .field("no_pfp_promise ready", &no_pfp_promise.ready().is_some())
.finish(),
Self::DoneLoading { url, cache_type } => f
.debug_struct("DoneLoading")
@@ -89,7 +83,12 @@ impl MediaAction {
/// Default processing logic for Media Actions. We don't handle ViewMedias here since
/// this may be app specific ?
- pub fn process_default_media_actions(self, images: &mut Images) {
+ pub fn process_default_media_actions(
+ self,
+ images: &mut Images,
+ jobs: &MediaJobSender,
+ ctx: &egui::Context,
+ ) {
match self {
MediaAction::ViewMedias(_urls) => {
// NOTE(jb55): don't assume we want to show a fullscreen
@@ -104,23 +103,22 @@ impl MediaAction {
//mview_state.set_urls(urls);
}
- MediaAction::FetchImage {
- url,
- cache_type,
- no_pfp_promise: promise,
- } => {
- images
- .get_cache_mut(cache_type)
- .textures_cache
- .insert_pending(&url, promise);
- }
- MediaAction::DoneLoading { url, cache_type } => {
- let cache = match cache_type {
- MediaCacheType::Image => &mut images.static_imgs,
- MediaCacheType::Gif => &mut images.gifs,
- };
-
- cache.textures_cache.move_to_loaded(&url);
+ MediaAction::FetchImage { url, cache_type } => match cache_type {
+ MediaCacheType::Image => {
+ images
+ .textures
+ .static_image
+ .request(jobs, ctx, &url, ImageType::Content(None))
+ }
+ MediaCacheType::Gif => {
+ images
+ .textures
+ .animated
+ .request(jobs, ctx, &url, ImageType::Content(None))
+ }
+ },
+ MediaAction::DoneLoading { url, cache_type: _ } => {
+ images.textures.blurred.finished_transitioning(&url);
}
}
}
diff --git a/crates/notedeck/src/media/blur.rs b/crates/notedeck/src/media/blur.rs
@@ -1,10 +1,15 @@
use std::collections::HashMap;
+use egui::TextureHandle;
use nostrdb::Note;
use crate::{
- jobs::{Job, JobError, JobParamsOwned},
+ jobs::{
+ CompleteResponse, JobOutput, JobPackage, JobRun, MediaJobKind, MediaJobResult,
+ MediaJobSender, RunType,
+ },
media::load_texture_checked,
+ TextureState,
};
#[derive(Clone)]
@@ -165,33 +170,6 @@ pub enum ObfuscationType {
Default,
}
-pub fn compute_blurhash(
- params: Option<JobParamsOwned>,
- dims: PixelDimensions,
-) -> Result<Job, JobError> {
- #[allow(irrefutable_let_patterns)]
- let Some(JobParamsOwned::Blurhash(params)) = params
- else {
- return Err(JobError::InvalidParameters);
- };
-
- let maybe_handle = match generate_blurhash_texturehandle(
- ¶ms.ctx,
- ¶ms.blurhash,
- ¶ms.url,
- dims.x,
- dims.y,
- ) {
- Ok(tex) => Some(tex),
- Err(e) => {
- tracing::error!("failed to render blurhash: {e}");
- None
- }
- };
-
- Ok(Job::Blurhash(maybe_handle))
-}
-
fn generate_blurhash_texturehandle(
ctx: &egui::Context,
blurhash: &str,
@@ -205,3 +183,81 @@ fn generate_blurhash_texturehandle(
let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
Ok(load_texture_checked(ctx, url, img, Default::default()))
}
+
+#[derive(Default)]
+pub struct BlurCache {
+ pub(crate) cache: HashMap<String, BlurState>,
+}
+
+pub struct BlurState {
+ pub tex_state: TextureState<TextureHandle>,
+ pub finished_transitioning: bool,
+}
+
+impl From<TextureState<TextureHandle>> for BlurState {
+ fn from(value: TextureState<TextureHandle>) -> Self {
+ BlurState {
+ tex_state: value,
+ finished_transitioning: false,
+ }
+ }
+}
+
+impl BlurCache {
+ pub fn get(&self, url: &str) -> Option<&BlurState> {
+ self.cache.get(url)
+ }
+
+ pub fn get_or_request(
+ &self,
+ jobs: &MediaJobSender,
+ ui: &egui::Ui,
+ url: &str,
+ blurhash: &ImageMetadata,
+ size: egui::Vec2,
+ ) -> &BlurState {
+ if let Some(res) = self.cache.get(url) {
+ return res;
+ }
+
+ let available_points = PointDimensions {
+ x: size.x,
+ y: size.y,
+ };
+ let pixel_sizes = blurhash.scaled_pixel_dimensions(ui, available_points);
+ let blurhash = blurhash.blurhash.to_owned();
+ let url = url.to_owned();
+ let ctx = ui.ctx().clone();
+
+ if let Err(e) = jobs.send(JobPackage::new(
+ url.to_owned(),
+ MediaJobKind::Blurhash,
+ RunType::Output(JobRun::Sync(Box::new(move || {
+ tracing::trace!("Starting blur job for {url}");
+ let res = generate_blurhash_texturehandle(
+ &ctx,
+ &blurhash,
+ &url,
+ pixel_sizes.x,
+ pixel_sizes.y,
+ );
+ JobOutput::Complete(CompleteResponse::new(MediaJobResult::Blurhash(res)))
+ }))),
+ )) {
+ tracing::error!("{e}");
+ }
+
+ &BlurState {
+ tex_state: TextureState::Pending,
+ finished_transitioning: false,
+ }
+ }
+
+ pub fn finished_transitioning(&mut self, url: &str) {
+ let Some(state) = self.cache.get_mut(url) else {
+ return;
+ };
+
+ state.finished_transitioning = true;
+ }
+}
diff --git a/crates/notedeck/src/media/gif.rs b/crates/notedeck/src/media/gif.rs
@@ -1,114 +1,45 @@
use std::{
- sync::mpsc::TryRecvError,
+ collections::{HashMap, VecDeque},
+ io::Cursor,
+ path::PathBuf,
time::{Instant, SystemTime},
};
-use crate::media::AnimationMode;
-use crate::Animation;
-use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
-use egui::TextureHandle;
+use crate::GifState;
+use crate::{
+ jobs::{
+ CompleteResponse, JobOutput, JobPackage, JobRun, MediaJobKind, MediaJobResult,
+ MediaJobSender, NoOutputRun, RunType,
+ },
+ media::{
+ images::{buffer_to_color_image, process_image},
+ load_texture_checked,
+ },
+ Error, ImageFrame, ImageType, MediaCache, TextureFrame, TextureState,
+};
+use crate::{media::AnimationMode, Animation};
+use egui::{ColorImage, TextureHandle};
+use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, Frame};
use std::time::Duration;
-pub fn ensure_latest_texture_from_cache(
- ui: &egui::Ui,
- url: &str,
- gifs: &mut GifStateMap,
- textures: &mut TexturesCache,
- animation_mode: AnimationMode,
-) -> Option<TextureHandle> {
- let tstate = textures.cache.get_mut(url)?;
-
- let TextureState::Loaded(img) = tstate.into() else {
- return None;
- };
-
- Some(ensure_latest_texture(ui, url, gifs, img, animation_mode))
-}
-
-struct ProcessedGifFrame {
- texture: TextureHandle,
- maybe_new_state: Option<GifState>,
- repaint_at: Option<SystemTime>,
+pub(crate) struct ProcessedGifFrame<'a> {
+ pub texture: &'a TextureHandle,
+ pub maybe_new_state: Option<GifState>,
+ pub repaint_at: Option<SystemTime>,
}
/// Process a gif state frame, and optionally present a new
/// state and when to repaint it
-fn process_gif_frame(
- animation: &Animation,
+pub(crate) fn process_gif_frame<'a>(
+ animation: &'a Animation,
frame_state: Option<&GifState>,
animation_mode: AnimationMode,
-) -> ProcessedGifFrame {
+) -> ProcessedGifFrame<'a> {
let now = Instant::now();
- match frame_state {
- Some(prev_state) => {
- let should_advance = animation_mode.can_animate()
- && (now - prev_state.last_frame_rendered >= prev_state.last_frame_duration);
-
- if should_advance {
- let maybe_new_index = if animation.receiver.is_some()
- || prev_state.last_frame_index < animation.num_frames() - 1
- {
- prev_state.last_frame_index + 1
- } else {
- 0
- };
-
- match animation.get_frame(maybe_new_index) {
- Some(frame) => {
- let next_frame_time = match animation_mode {
- AnimationMode::Continuous { fps } => match fps {
- Some(fps) => {
- let max_delay_ms = Duration::from_millis((1000.0 / fps) as u64);
- SystemTime::now().checked_add(frame.delay.max(max_delay_ms))
- }
- None => SystemTime::now().checked_add(frame.delay),
- },
-
- AnimationMode::NoAnimation | AnimationMode::Reactive => None,
- };
-
- ProcessedGifFrame {
- texture: frame.texture.clone(),
- maybe_new_state: Some(GifState {
- last_frame_rendered: now,
- last_frame_duration: frame.delay,
- next_frame_time,
- last_frame_index: maybe_new_index,
- }),
- repaint_at: next_frame_time,
- }
- }
- None => {
- let (texture, maybe_new_state) =
- match animation.get_frame(prev_state.last_frame_index) {
- Some(frame) => (frame.texture.clone(), None),
- None => (animation.first_frame.texture.clone(), None),
- };
-
- ProcessedGifFrame {
- texture,
- maybe_new_state,
- repaint_at: prev_state.next_frame_time,
- }
- }
- }
- } else {
- let (texture, maybe_new_state) =
- match animation.get_frame(prev_state.last_frame_index) {
- Some(frame) => (frame.texture.clone(), None),
- None => (animation.first_frame.texture.clone(), None),
- };
-
- ProcessedGifFrame {
- texture,
- maybe_new_state,
- repaint_at: prev_state.next_frame_time,
- }
- }
- }
- None => ProcessedGifFrame {
- texture: animation.first_frame.texture.clone(),
+ let Some(prev_state) = frame_state else {
+ return ProcessedGifFrame {
+ texture: &animation.first_frame.texture,
maybe_new_state: Some(GifState {
last_frame_rendered: now,
last_frame_duration: animation.first_frame.delay,
@@ -116,49 +47,274 @@ fn process_gif_frame(
last_frame_index: 0,
}),
repaint_at: None,
+ };
+ };
+
+ let should_advance = animation_mode.can_animate()
+ && (now - prev_state.last_frame_rendered >= prev_state.last_frame_duration);
+
+ if !should_advance {
+ let (texture, maybe_new_state) = match animation.get_frame(prev_state.last_frame_index) {
+ Some(frame) => (&frame.texture, None),
+ None => (&animation.first_frame.texture, None),
+ };
+
+ return ProcessedGifFrame {
+ texture,
+ maybe_new_state,
+ repaint_at: prev_state.next_frame_time,
+ };
+ }
+
+ let maybe_new_index = if prev_state.last_frame_index < animation.num_frames() - 1 {
+ prev_state.last_frame_index + 1
+ } else {
+ 0
+ };
+
+ let Some(frame) = animation.get_frame(maybe_new_index) else {
+ let (texture, maybe_new_state) = match animation.get_frame(prev_state.last_frame_index) {
+ Some(frame) => (&frame.texture, None),
+ None => (&animation.first_frame.texture, None),
+ };
+
+ return ProcessedGifFrame {
+ texture,
+ maybe_new_state,
+ repaint_at: prev_state.next_frame_time,
+ };
+ };
+
+ let next_frame_time = match animation_mode {
+ AnimationMode::Continuous { fps } => match fps {
+ Some(fps) => {
+ let max_delay_ms = Duration::from_millis((1000.0 / fps) as u64);
+ SystemTime::now().checked_add(frame.delay.max(max_delay_ms))
+ }
+ None => SystemTime::now().checked_add(frame.delay),
},
+
+ AnimationMode::NoAnimation | AnimationMode::Reactive => None,
+ };
+
+ ProcessedGifFrame {
+ texture: &frame.texture,
+ maybe_new_state: Some(GifState {
+ last_frame_rendered: now,
+ last_frame_duration: frame.delay,
+ next_frame_time,
+ last_frame_index: maybe_new_index,
+ }),
+ repaint_at: next_frame_time,
}
}
-pub fn ensure_latest_texture(
- ui: &egui::Ui,
- url: &str,
- gifs: &mut GifStateMap,
- img: &mut TexturedImage,
- animation_mode: AnimationMode,
-) -> TextureHandle {
- match img {
- TexturedImage::Static(handle) => handle.clone(),
- TexturedImage::Animated(animation) => {
- if let Some(receiver) = &animation.receiver {
- loop {
- match receiver.try_recv() {
- Ok(frame) => animation.other_frames.push(frame),
- Err(TryRecvError::Empty) => {
- break;
- }
- Err(TryRecvError::Disconnected) => {
- animation.receiver = None;
- break;
- }
- }
- }
- }
+pub struct AnimatedImgTexCache {
+ pub(crate) cache: HashMap<String, TextureState<Animation>>,
+ animated_img_cache_path: PathBuf,
+}
- let next_state = process_gif_frame(animation, gifs.get(url), animation_mode);
+impl AnimatedImgTexCache {
+ pub fn new(animated_img_cache_path: PathBuf) -> Self {
+ Self {
+ cache: Default::default(),
+ animated_img_cache_path,
+ }
+ }
+
+ pub fn contains(&self, url: &str) -> bool {
+ self.cache.contains_key(url)
+ }
- if let Some(new_state) = next_state.maybe_new_state {
- gifs.insert(url.to_owned(), new_state);
+ pub fn get(&self, url: &str) -> Option<&TextureState<Animation>> {
+ self.cache.get(url)
+ }
+
+ pub fn request(
+ &self,
+ jobs: &MediaJobSender,
+ ctx: &egui::Context,
+ url: &str,
+ imgtype: ImageType,
+ ) {
+ let _ = self.get_or_request(jobs, ctx, url, imgtype);
+ }
+
+ pub fn get_or_request(
+ &self,
+ jobs: &MediaJobSender,
+ ctx: &egui::Context,
+ url: &str,
+ imgtype: ImageType,
+ ) -> &TextureState<Animation> {
+ if let Some(res) = self.cache.get(url) {
+ return res;
+ };
+
+ let key = MediaCache::key(url);
+ let path = self.animated_img_cache_path.join(key);
+ let ctx = ctx.clone();
+ let url = url.to_owned();
+ if path.exists() {
+ if let Err(e) = jobs.send(JobPackage::new(
+ url.to_owned(),
+ MediaJobKind::AnimatedImg,
+ RunType::Output(JobRun::Sync(Box::new(move || {
+ from_disk_job_run(ctx, url, path)
+ }))),
+ )) {
+ tracing::error!("{e}");
}
+ } else {
+ let anim_path = self.animated_img_cache_path.clone();
+ if let Err(e) = jobs.send(JobPackage::new(
+ url.to_owned(),
+ MediaJobKind::AnimatedImg,
+ RunType::Output(JobRun::Async(Box::pin(from_net_run(
+ ctx, url, anim_path, imgtype,
+ )))),
+ )) {
+ tracing::error!("{e}");
+ }
+ }
+
+ &TextureState::Pending
+ }
+}
- if let Some(repaint) = next_state.repaint_at {
- tracing::trace!("requesting repaint for {url} after {repaint:?}");
- if let Ok(dur) = repaint.duration_since(SystemTime::now()) {
- ui.ctx().request_repaint_after(dur);
+fn from_disk_job_run(ctx: egui::Context, url: String, path: PathBuf) -> JobOutput<MediaJobResult> {
+ tracing::trace!("Starting animated from disk job for {url}");
+ let gif_bytes = match std::fs::read(path.clone()) {
+ Ok(b) => b,
+ Err(e) => {
+ return JobOutput::Complete(CompleteResponse::new(MediaJobResult::Animation(Err(
+ Error::Io(e),
+ ))))
+ }
+ };
+ JobOutput::Complete(CompleteResponse::new(MediaJobResult::Animation(
+ generate_anim_pkg(ctx.clone(), url.to_owned(), gif_bytes, |img| {
+ buffer_to_color_image(img.as_flat_samples_u8(), img.width(), img.height())
+ })
+ .map(|f| f.anim),
+ )))
+}
+
+async fn from_net_run(
+ ctx: egui::Context,
+ url: String,
+ path: PathBuf,
+ imgtype: ImageType,
+) -> JobOutput<MediaJobResult> {
+ let res = match crate::media::network::http_req(&url).await {
+ Ok(r) => r,
+ Err(e) => {
+ return JobOutput::complete(MediaJobResult::Animation(Err(crate::Error::Generic(
+ format!("Http error: {e}"),
+ ))));
+ }
+ };
+
+ JobOutput::Next(JobRun::Sync(Box::new(move || {
+ tracing::trace!("Starting animated img from net job for {url}");
+ let animation =
+ match generate_anim_pkg(ctx.clone(), url.to_owned(), res.bytes, move |img| {
+ process_image(imgtype, img)
+ }) {
+ Ok(a) => a,
+ Err(e) => {
+ return JobOutput::Complete(CompleteResponse::new(MediaJobResult::Animation(
+ Err(e),
+ )));
}
- }
+ };
+ JobOutput::Complete(
+ CompleteResponse::new(MediaJobResult::Animation(Ok(animation.anim))).run_no_output(
+ NoOutputRun::Sync(Box::new(move || {
+ tracing::trace!("writing animated texture to file for {url}");
+ if let Err(e) = MediaCache::write_gif(&path, &url, animation.img_frames) {
+ tracing::error!("Could not write gif to disk: {e}");
+ }
+ })),
+ ),
+ )
+ })))
+}
+
+fn generate_anim_pkg(
+ ctx: egui::Context,
+ url: String,
+ gif_bytes: Vec<u8>,
+ process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
+) -> Result<AnimationPackage, Error> {
+ let decoder = {
+ let reader = Cursor::new(gif_bytes.as_slice());
+ GifDecoder::new(reader)?
+ };
+
+ let frames: VecDeque<Frame> = decoder
+ .into_frames()
+ .collect::<std::result::Result<VecDeque<_>, image::ImageError>>()
+ .map_err(|e| crate::Error::Generic(e.to_string()))?;
+
+ let mut imgs = Vec::new();
+ let mut other_frames = Vec::new();
- next_state.texture
+ let mut first_frame = None;
+ for (i, frame) in frames.into_iter().enumerate() {
+ let delay = frame.delay();
+ let img = generate_color_img_frame(frame, process_to_egui);
+ imgs.push(ImageFrame {
+ delay: delay.into(),
+ image: img.clone(),
+ });
+
+ let tex_frame = generate_animation_frame(&ctx, &url, i, delay.into(), img);
+
+ if first_frame.is_none() {
+ first_frame = Some(tex_frame);
+ } else {
+ other_frames.push(tex_frame);
}
}
+
+ let Some(first_frame) = first_frame else {
+ return Err(crate::Error::Generic(
+ "first frame not found for gif".to_owned(),
+ ));
+ };
+
+ Ok(AnimationPackage {
+ anim: Animation {
+ first_frame,
+ other_frames,
+ },
+ img_frames: imgs,
+ })
+}
+
+struct AnimationPackage {
+ anim: Animation,
+ img_frames: Vec<ImageFrame>,
+}
+
+fn generate_color_img_frame(
+ frame: image::Frame,
+ process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
+) -> ColorImage {
+ let img = DynamicImage::ImageRgba8(frame.into_buffer());
+ process_to_egui(img)
+}
+
+fn generate_animation_frame(
+ ctx: &egui::Context,
+ url: &str,
+ index: usize,
+ delay: Duration,
+ color_img: ColorImage,
+) -> TextureFrame {
+ TextureFrame {
+ delay,
+ texture: load_texture_checked(ctx, format!("{url}{index}"), color_img, Default::default()),
+ }
}
diff --git a/crates/notedeck/src/media/images.rs b/crates/notedeck/src/media/images.rs
@@ -1,19 +1,8 @@
-use crate::media::load_texture_checked;
-use crate::{Animation, ImageFrame, MediaCache, MediaCacheType, TextureFrame, TexturedImage};
-use egui::{pos2, Color32, ColorImage, Context, Rect, Sense, SizeHint};
-use image::codecs::gif::GifDecoder;
+use crate::media::network::HyperHttpResponse;
+use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint};
use image::imageops::FilterType;
-use image::{AnimationDecoder, DynamicImage, FlatSamples, Frame};
-use poll_promise::Promise;
-use std::collections::VecDeque;
-use std::io::Cursor;
+use image::FlatSamples;
use std::path::PathBuf;
-use std::path::{self, Path};
-use std::sync::mpsc;
-use std::sync::mpsc::SyncSender;
-use std::thread;
-use std::time::Duration;
-use tokio::fs;
// NOTE(jb55): chatgpt wrote this because I was too dumb to
pub fn aspect_fill(
@@ -137,7 +126,7 @@ fn resize_image_if_too_big(
/// - resize if any larger, using [`resize_image_if_too_big`]
///
#[profiling::function]
-fn process_image(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
+pub fn process_image(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
const MAX_IMG_LENGTH: u32 = 2048;
const FILTER_TYPE: FilterType = FilterType::CatmullRom;
@@ -184,11 +173,11 @@ fn process_image(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImag
}
#[profiling::function]
-fn parse_img_response(
- response: ehttp::Response,
+pub fn parse_img_response(
+ response: HyperHttpResponse,
imgtyp: ImageType,
) -> Result<ColorImage, crate::Error> {
- let content_type = response.content_type().unwrap_or_default();
+ let content_type = response.content_type.unwrap_or_default();
let size_hint = match imgtyp {
ImageType::Profile(size) => SizeHint::Size(size, size),
ImageType::Content(Some((w, h))) => SizeHint::Size(w, h),
@@ -211,167 +200,7 @@ fn parse_img_response(
}
}
-fn fetch_img_from_disk(
- ctx: &egui::Context,
- url: &str,
- path: &path::Path,
- cache_type: MediaCacheType,
-) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
- let ctx = ctx.clone();
- let url = url.to_owned();
- let path = path.to_owned();
-
- Promise::spawn_async(async move {
- Some(async_fetch_img_from_disk(ctx, url, &path, cache_type).await)
- })
-}
-
-async fn async_fetch_img_from_disk(
- ctx: egui::Context,
- url: String,
- path: &path::Path,
- cache_type: MediaCacheType,
-) -> Result<TexturedImage, crate::Error> {
- match cache_type {
- MediaCacheType::Image => {
- let data = fs::read(path).await?;
- let image_buffer = image::load_from_memory(&data).map_err(crate::Error::Image)?;
-
- let img = buffer_to_color_image(
- image_buffer.as_flat_samples_u8(),
- image_buffer.width(),
- image_buffer.height(),
- );
- Ok(TexturedImage::Static(load_texture_checked(
- &ctx,
- &url,
- img,
- Default::default(),
- )))
- }
- MediaCacheType::Gif => {
- let gif_bytes = fs::read(path).await?; // Read entire file into a Vec<u8>
- generate_gif(ctx, url, path, gif_bytes, false, |i| {
- buffer_to_color_image(i.as_flat_samples_u8(), i.width(), i.height())
- })
- }
- }
-}
-
-fn generate_gif(
- ctx: egui::Context,
- url: String,
- path: &path::Path,
- data: Vec<u8>,
- write_to_disk: bool,
- process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
-) -> Result<TexturedImage, crate::Error> {
- let decoder = {
- let reader = Cursor::new(data.as_slice());
- GifDecoder::new(reader)?
- };
- let (tex_input, tex_output) = mpsc::sync_channel(4);
- let (maybe_encoder_input, maybe_encoder_output) = if write_to_disk {
- let (inp, out) = mpsc::sync_channel(4);
- (Some(inp), Some(out))
- } else {
- (None, None)
- };
-
- let mut frames: VecDeque<Frame> = decoder
- .into_frames()
- .collect::<std::result::Result<VecDeque<_>, image::ImageError>>()
- .map_err(|e| crate::Error::Generic(e.to_string()))?;
-
- let first_frame = frames.pop_front().map(|frame| {
- generate_animation_frame(
- &ctx,
- &url,
- 0,
- frame,
- maybe_encoder_input.as_ref(),
- process_to_egui,
- )
- });
-
- let cur_url = url.clone();
- thread::spawn(move || {
- for (index, frame) in frames.into_iter().enumerate() {
- let texture_frame = generate_animation_frame(
- &ctx,
- &cur_url,
- index,
- frame,
- maybe_encoder_input.as_ref(),
- process_to_egui,
- );
-
- if tex_input.send(texture_frame).is_err() {
- //tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
- break;
- }
- }
- });
-
- if let Some(encoder_output) = maybe_encoder_output {
- let path = path.to_owned();
-
- thread::spawn(move || {
- let mut imgs = Vec::new();
- while let Ok(img) = encoder_output.recv() {
- imgs.push(img);
- }
-
- if let Err(e) = MediaCache::write_gif(&path, &url, imgs) {
- tracing::error!("Could not write gif to disk: {e}");
- }
- });
- }
-
- first_frame.map_or_else(
- || {
- Err(crate::Error::Generic(
- "first frame not found for gif".to_owned(),
- ))
- },
- |first_frame| {
- Ok(TexturedImage::Animated(Animation {
- other_frames: Default::default(),
- receiver: Some(tex_output),
- first_frame,
- }))
- },
- )
-}
-
-fn generate_animation_frame(
- ctx: &egui::Context,
- url: &str,
- index: usize,
- frame: image::Frame,
- maybe_encoder_input: Option<&SyncSender<ImageFrame>>,
- process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + 'static,
-) -> TextureFrame {
- let delay = Duration::from(frame.delay());
- let img = DynamicImage::ImageRgba8(frame.into_buffer());
- let color_img = process_to_egui(img);
-
- if let Some(sender) = maybe_encoder_input {
- if let Err(e) = sender.send(ImageFrame {
- delay,
- image: color_img.clone(),
- }) {
- tracing::error!("ImageFrame mpsc unexpectedly closed: {e}");
- }
- }
-
- TextureFrame {
- delay,
- texture: load_texture_checked(ctx, format!("{url}{index}"), color_img, Default::default()),
- }
-}
-
-fn buffer_to_color_image(
+pub fn buffer_to_color_image(
samples: Option<FlatSamples<&[u8]>>,
width: u32,
height: u32,
@@ -393,89 +222,3 @@ pub enum ImageType {
/// Content Image with optional size hint
Content(Option<(u32, u32)>),
}
-
-pub fn fetch_img(
- img_cache_path: &Path,
- ctx: &egui::Context,
- url: &str,
- imgtyp: ImageType,
- cache_type: MediaCacheType,
-) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
- let key = MediaCache::key(url);
- let path = img_cache_path.join(key);
-
- if path.exists() {
- fetch_img_from_disk(ctx, url, &path, cache_type)
- } else {
- fetch_img_from_net(img_cache_path, ctx, url, imgtyp, cache_type)
- }
-
- // TODO: fetch image from local cache
-}
-
-fn fetch_img_from_net(
- cache_path: &path::Path,
- ctx: &egui::Context,
- url: &str,
- imgtyp: ImageType,
- cache_type: MediaCacheType,
-) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
- let (sender, promise) = Promise::new();
- let request = ehttp::Request::get(url);
- let ctx = ctx.clone();
- let cloned_url = url.to_owned();
- let cache_path = cache_path.to_owned();
- ehttp::fetch(request, move |response| {
- let handle = response.map_err(crate::Error::Generic).and_then(|resp| {
- match cache_type {
- MediaCacheType::Image => {
- let img = parse_img_response(resp, imgtyp);
- img.map(|img| {
- let texture_handle = load_texture_checked(
- &ctx,
- &cloned_url,
- img.clone(),
- Default::default(),
- );
-
- // write to disk
- std::thread::spawn(move || {
- MediaCache::write(&cache_path, &cloned_url, img)
- });
-
- TexturedImage::Static(texture_handle)
- })
- }
- MediaCacheType::Gif => {
- let gif_bytes = resp.bytes;
- generate_gif(
- ctx.clone(),
- cloned_url,
- &cache_path,
- gif_bytes,
- true,
- move |img| process_image(imgtyp, img),
- )
- }
- }
- });
-
- sender.send(Some(handle)); // send the results back to the UI thread.
- ctx.request_repaint();
- });
-
- promise
-}
-
-pub fn fetch_no_pfp_promise(
- ctx: &Context,
- cache: &MediaCache,
-) -> Promise<Option<Result<TexturedImage, crate::Error>>> {
- crate::media::images::fetch_img(
- &cache.cache_dir,
- ctx,
- crate::profile::no_pfp_url(),
- ImageType::Profile(128),
- MediaCacheType::Image,
- )
-}
diff --git a/crates/notedeck/src/media/latest.rs b/crates/notedeck/src/media/latest.rs
@@ -0,0 +1,229 @@
+use std::time::SystemTime;
+
+use egui::TextureHandle;
+
+use crate::jobs::MediaJobSender;
+use crate::{
+ media::{
+ gif::{process_gif_frame, AnimatedImgTexCache},
+ static_imgs::StaticImgTexCache,
+ AnimationMode, BlurCache,
+ },
+ Error, GifStateMap, ImageType, MediaCacheType, ObfuscationType, TextureState,
+};
+
+pub enum MediaRenderState<'a> {
+ ActualImage(&'a TextureHandle),
+ Transitioning {
+ image: &'a TextureHandle,
+ obfuscation: ObfuscatedTexture<'a>,
+ },
+ Error(&'a crate::Error),
+ Shimmering(ObfuscatedTexture<'a>),
+ Obfuscated(ObfuscatedTexture<'a>),
+}
+
+pub enum ObfuscatedTexture<'a> {
+ Blur(&'a TextureHandle),
+ Default,
+}
+
+pub struct NoLoadingLatestTex<'a> {
+ static_cache: &'a StaticImgTexCache,
+ animated_cache: &'a AnimatedImgTexCache,
+ gif_state: &'a mut GifStateMap,
+}
+
+impl<'a> NoLoadingLatestTex<'a> {
+ pub fn new(
+ static_cache: &'a StaticImgTexCache,
+ animated_cache: &'a AnimatedImgTexCache,
+ gif_state: &'a mut GifStateMap,
+ ) -> Self {
+ Self {
+ static_cache,
+ animated_cache,
+ gif_state,
+ }
+ }
+
+ pub fn latest(
+ &mut self,
+ jobs: &MediaJobSender,
+ ctx: &egui::Context,
+ url: &str,
+ cache_type: MediaCacheType,
+ imgtype: ImageType,
+ animation_mode: AnimationMode,
+ ) -> Option<&'a TextureHandle> {
+ let LatestImageTex::Loaded(tex) =
+ self.latest_state(jobs, ctx, url, cache_type, imgtype, animation_mode)
+ else {
+ return None;
+ };
+
+ Some(tex)
+ }
+
+ pub fn latest_state(
+ &mut self,
+ jobs: &MediaJobSender,
+ ctx: &egui::Context,
+ url: &str,
+ cache_type: MediaCacheType,
+ imgtype: ImageType,
+ animation_mode: AnimationMode,
+ ) -> LatestImageTex<'a> {
+ match cache_type {
+ MediaCacheType::Image => {
+ match self.static_cache.get_or_request(jobs, ctx, url, imgtype) {
+ TextureState::Pending => LatestImageTex::Pending,
+ TextureState::Error(error) => LatestImageTex::Error(error),
+ TextureState::Loaded(t) => LatestImageTex::Loaded(t),
+ }
+ }
+ MediaCacheType::Gif => {
+ match self.animated_cache.get_or_request(jobs, ctx, url, imgtype) {
+ TextureState::Pending => LatestImageTex::Pending,
+ TextureState::Error(error) => LatestImageTex::Error(error),
+ TextureState::Loaded(animation) => {
+ let next_state =
+ process_gif_frame(animation, self.gif_state.get(url), animation_mode);
+
+ if let Some(new_state) = next_state.maybe_new_state {
+ self.gif_state.insert(url.to_owned(), new_state);
+ }
+
+ if let Some(repaint) = next_state.repaint_at {
+ tracing::trace!("requesting repaint for {url} after {repaint:?}");
+ if let Ok(dur) = repaint.duration_since(SystemTime::now()) {
+ ctx.request_repaint_after(dur);
+ }
+ }
+
+ LatestImageTex::Loaded(next_state.texture)
+ }
+ }
+ }
+ }
+ }
+}
+
+pub enum LatestImageTex<'a> {
+ Pending,
+ Error(&'a Error),
+ Loaded(&'a TextureHandle),
+}
+
+pub struct UntrustedMediaLatestTex<'a> {
+ blur_cache: &'a BlurCache,
+}
+
+/// Media is untrusted and should only show a blur of the underlying media
+impl<'a> UntrustedMediaLatestTex<'a> {
+ pub fn new(blur_cache: &'a BlurCache) -> Self {
+ Self { blur_cache }
+ }
+
+ pub fn latest(
+ &self,
+ jobs: &MediaJobSender,
+ ui: &egui::Ui,
+ url: &str,
+ obfuscation_type: &'a ObfuscationType,
+ size: egui::Vec2,
+ ) -> MediaRenderState<'a> {
+ MediaRenderState::Obfuscated(self.latest_internal(jobs, ui, url, obfuscation_type, size))
+ }
+
+ fn latest_internal(
+ &self,
+ jobs: &MediaJobSender,
+ ui: &egui::Ui,
+ url: &str,
+ obfuscation_type: &'a ObfuscationType,
+ size: egui::Vec2,
+ ) -> ObfuscatedTexture<'a> {
+ let ObfuscationType::Blurhash(meta) = obfuscation_type else {
+ return ObfuscatedTexture::Default;
+ };
+
+ let state = self.blur_cache.get_or_request(jobs, ui, url, meta, size);
+
+ match &state.tex_state {
+ TextureState::Pending | TextureState::Error(_) => ObfuscatedTexture::Default,
+ TextureState::Loaded(t) => ObfuscatedTexture::Blur(t),
+ }
+ }
+}
+
+/// Media is trusted and should be loaded ASAP
+pub struct TrustedMediaLatestTex<'a> {
+ img_no_loading: NoLoadingLatestTex<'a>,
+ blur_cache: &'a BlurCache,
+}
+
+impl<'a> TrustedMediaLatestTex<'a> {
+ pub fn new(img_no_loading: NoLoadingLatestTex<'a>, blur_cache: &'a BlurCache) -> Self {
+ Self {
+ img_no_loading,
+ blur_cache,
+ }
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ pub fn latest(
+ &mut self,
+ jobs: &MediaJobSender,
+ ui: &egui::Ui,
+ url: &str,
+ cache_type: MediaCacheType,
+ imgtype: ImageType,
+ animation_mode: AnimationMode,
+ obfuscation_type: &'a ObfuscationType,
+ size: egui::Vec2,
+ ) -> MediaRenderState<'a> {
+ let actual_latest_tex = self.img_no_loading.latest_state(
+ jobs,
+ ui.ctx(),
+ url,
+ cache_type,
+ imgtype,
+ animation_mode,
+ );
+
+ match actual_latest_tex {
+ LatestImageTex::Pending => (),
+ LatestImageTex::Error(error) => return MediaRenderState::Error(error),
+ LatestImageTex::Loaded(texture_handle) => {
+ let Some(blur) = self.blur_cache.get(url) else {
+ return MediaRenderState::ActualImage(texture_handle);
+ };
+
+ if blur.finished_transitioning {
+ return MediaRenderState::ActualImage(texture_handle);
+ };
+
+ let obfuscation = match &blur.tex_state {
+ TextureState::Pending | TextureState::Error(_) => ObfuscatedTexture::Default,
+ TextureState::Loaded(t) => ObfuscatedTexture::Blur(t),
+ };
+
+ return MediaRenderState::Transitioning {
+ image: texture_handle,
+ obfuscation,
+ };
+ }
+ };
+
+ MediaRenderState::Shimmering(
+ UntrustedMediaLatestTex::new(self.blur_cache).latest_internal(
+ jobs,
+ ui,
+ url,
+ obfuscation_type,
+ size,
+ ),
+ )
+ }
+}
diff --git a/crates/notedeck/src/media/mod.rs b/crates/notedeck/src/media/mod.rs
@@ -3,15 +3,21 @@ pub mod blur;
pub mod gif;
pub mod images;
pub mod imeta;
+pub mod latest;
+pub mod network;
pub mod renderable;
+pub mod static_imgs;
pub use action::{MediaAction, MediaInfo, ViewMediaInfo};
pub use blur::{
- compute_blurhash, update_imeta_blurhashes, ImageMetadata, ObfuscationType, PixelDimensions,
+ update_imeta_blurhashes, BlurCache, ImageMetadata, ObfuscationType, PixelDimensions,
PointDimensions,
};
use egui::{ColorImage, TextureHandle};
pub use images::ImageType;
+pub use latest::{
+ MediaRenderState, NoLoadingLatestTex, TrustedMediaLatestTex, UntrustedMediaLatestTex,
+};
pub use renderable::RenderableMedia;
#[derive(Copy, Clone, Debug)]
diff --git a/crates/notedeck/src/media/network.rs b/crates/notedeck/src/media/network.rs
@@ -0,0 +1,177 @@
+use std::{error::Error, fmt};
+
+use http_body_util::{BodyExt, Empty};
+use hyper::{
+ body::Bytes,
+ header::{self},
+ Request, Uri,
+};
+use hyper_rustls::HttpsConnectorBuilder;
+use hyper_util::{client::legacy::Client, rt::TokioExecutor};
+use url::Url;
+
+const MAX_BODY_BYTES: usize = 20 * 1024 * 1024;
+
+pub async fn http_req(url: &str) -> Result<HyperHttpResponse, HyperHttpError> {
+ let mut current_uri: Uri = url.parse().map_err(|_| HyperHttpError::Uri)?;
+
+ let https = {
+ let builder = match HttpsConnectorBuilder::new().with_native_roots() {
+ Ok(builder) => builder,
+ Err(err) => {
+ tracing::warn!(
+ "Failed to load native root certificates ({err}). Falling back to WebPKI store."
+ );
+ HttpsConnectorBuilder::new().with_webpki_roots()
+ }
+ };
+
+ builder.https_or_http().enable_http1().build()
+ };
+
+ let client: Client<_, Empty<Bytes>> = Client::builder(TokioExecutor::new()).build(https);
+
+ const MAX_REDIRECTS: usize = 5;
+ let mut redirects = 0;
+
+ let res = loop {
+ let authority = current_uri.authority().ok_or(HyperHttpError::Host)?.clone();
+
+ // Fetch the url...
+ let req = Request::builder()
+ .uri(current_uri.clone())
+ .header(hyper::header::HOST, authority.as_str())
+ .body(Empty::<Bytes>::new())
+ .map_err(|e| HyperHttpError::Hyper(Box::new(e)))?;
+
+ let res = client
+ .request(req)
+ .await
+ .map_err(|e| HyperHttpError::Hyper(Box::new(e)))?;
+
+ if res.status().is_redirection() {
+ if redirects >= MAX_REDIRECTS {
+ return Err(HyperHttpError::TooManyRedirects);
+ }
+
+ let location_header = res
+ .headers()
+ .get(header::LOCATION)
+ .ok_or(HyperHttpError::MissingRedirectLocation)?
+ .clone();
+
+ let location = location_header
+ .to_str()
+ .map_err(|_| HyperHttpError::InvalidRedirectLocation)?
+ .to_string();
+
+ res.into_body()
+ .collect()
+ .await
+ .map_err(|e| HyperHttpError::Hyper(Box::new(e)))?;
+
+ current_uri = resolve_redirect(¤t_uri, &location)?;
+ redirects += 1;
+ continue;
+ } else {
+ break res;
+ }
+ };
+
+ let content_type = res
+ .headers()
+ .get(hyper::header::CONTENT_TYPE)
+ .and_then(|t| t.to_str().ok())
+ .map(|s| s.to_string());
+
+ let content_length = res
+ .headers()
+ .get(header::CONTENT_LENGTH)
+ .and_then(|s| s.to_str().ok())
+ .and_then(|s| s.parse::<usize>().ok());
+
+ if let Some(len) = content_length {
+ if len > MAX_BODY_BYTES {
+ return Err(HyperHttpError::BodyTooLarge);
+ }
+ }
+
+ let mut body = res.into_body();
+ let mut bytes = Vec::with_capacity(content_length.unwrap_or(0).min(MAX_BODY_BYTES));
+
+ while let Some(frame_result) = body.frame().await {
+ let frame = frame_result.map_err(|e| HyperHttpError::Hyper(Box::new(e)))?;
+ let Ok(chunk) = frame.into_data() else {
+ continue;
+ };
+
+ if bytes.len() + chunk.len() > MAX_BODY_BYTES {
+ return Err(HyperHttpError::BodyTooLarge);
+ }
+
+ bytes.extend_from_slice(&chunk);
+ }
+
+ Ok(HyperHttpResponse {
+ content_type,
+ bytes,
+ })
+}
+
+#[derive(Debug)]
+pub enum HyperHttpError {
+ Hyper(Box<dyn std::error::Error + Send + Sync>),
+ Host,
+ Uri,
+ BodyTooLarge,
+ TooManyRedirects,
+ MissingRedirectLocation,
+ InvalidRedirectLocation,
+}
+
+#[derive(Debug)]
+pub struct HyperHttpResponse {
+ pub content_type: Option<String>,
+ pub bytes: Vec<u8>,
+}
+
+impl Error for HyperHttpError {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ match self {
+ Self::Hyper(e) => Some(&**e),
+ _ => None,
+ }
+ }
+}
+
+impl fmt::Display for HyperHttpError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Hyper(e) => write!(f, "Hyper error: {}", e),
+ Self::Host => write!(f, "Missing host in URL"),
+ Self::Uri => write!(f, "Invalid URI"),
+ Self::BodyTooLarge => write!(f, "Body too large"),
+ Self::TooManyRedirects => write!(f, "Too many redirect responses"),
+ Self::MissingRedirectLocation => write!(f, "Redirect response missing Location header"),
+ Self::InvalidRedirectLocation => write!(f, "Invalid redirect Location header"),
+ }
+ }
+}
+
+fn resolve_redirect(current: &Uri, location: &str) -> Result<Uri, HyperHttpError> {
+ if let Ok(uri) = location.parse::<Uri>() {
+ if uri.scheme().is_some() {
+ return Ok(uri);
+ }
+ }
+
+ let base = Url::parse(¤t.to_string()).map_err(|_| HyperHttpError::Uri)?;
+ let joined = base
+ .join(location)
+ .map_err(|_| HyperHttpError::InvalidRedirectLocation)?;
+
+ joined
+ .as_str()
+ .parse::<Uri>()
+ .map_err(|_| HyperHttpError::InvalidRedirectLocation)
+}
diff --git a/crates/notedeck/src/media/static_imgs.rs b/crates/notedeck/src/media/static_imgs.rs
@@ -0,0 +1,172 @@
+use std::{
+ collections::HashMap,
+ path::{Path, PathBuf},
+};
+
+use egui::TextureHandle;
+
+use crate::{jobs::NoOutputRun, TextureState};
+use crate::{
+ jobs::{
+ CompleteResponse, JobOutput, JobPackage, JobRun, MediaJobKind, MediaJobResult,
+ MediaJobSender, RunType,
+ },
+ ImageType,
+};
+use crate::{
+ media::{
+ images::{buffer_to_color_image, parse_img_response},
+ load_texture_checked,
+ network::http_req,
+ },
+ MediaCache,
+};
+
+pub struct StaticImgTexCache {
+ pub(crate) cache: HashMap<String, TextureState<TextureHandle>>,
+ static_img_cache_path: PathBuf,
+}
+
+impl StaticImgTexCache {
+ pub fn new(static_img_cache_path: PathBuf) -> Self {
+ Self {
+ cache: Default::default(),
+ static_img_cache_path,
+ }
+ }
+
+ pub fn contains(&self, url: &str) -> bool {
+ self.cache.contains_key(url)
+ }
+
+ pub fn get(&self, url: &str) -> Option<&TextureState<TextureHandle>> {
+ self.cache.get(url)
+ }
+
+ pub fn request(
+ &self,
+ jobs: &MediaJobSender,
+ ctx: &egui::Context,
+ url: &str,
+ imgtype: ImageType,
+ ) {
+ let _ = self.get_or_request(jobs, ctx, url, imgtype);
+ }
+
+ pub fn get_or_request(
+ &self,
+ jobs: &MediaJobSender,
+ ctx: &egui::Context,
+ url: &str,
+ imgtype: ImageType,
+ ) -> &TextureState<TextureHandle> {
+ if let Some(res) = self.cache.get(url) {
+ return res;
+ }
+
+ let key = MediaCache::key(url);
+ let path = self.static_img_cache_path.join(key);
+
+ if path.exists() {
+ let ctx = ctx.clone();
+ let url = url.to_owned();
+ if let Err(e) = jobs.send(JobPackage::new(
+ url.to_owned(),
+ MediaJobKind::StaticImg,
+ RunType::Output(JobRun::Sync(Box::new(move || {
+ JobOutput::Complete(CompleteResponse::new(MediaJobResult::StaticImg(
+ fetch_static_img_from_disk(ctx.clone(), &url, &path),
+ )))
+ }))),
+ )) {
+ tracing::error!("{e}");
+ }
+ } else {
+ let url = url.to_owned();
+ let ctx = ctx.clone();
+ if let Err(e) = jobs.send(JobPackage::new(
+ url.to_owned(),
+ MediaJobKind::StaticImg,
+ RunType::Output(JobRun::Async(Box::pin(fetch_static_img_from_net(
+ url,
+ ctx,
+ self.static_img_cache_path.clone(),
+ imgtype,
+ )))),
+ )) {
+ tracing::error!("{e}");
+ }
+ }
+
+ &TextureState::Pending
+ }
+}
+
+pub fn fetch_static_img_from_disk(
+ ctx: egui::Context,
+ url: &str,
+ path: &Path,
+) -> Result<egui::TextureHandle, crate::Error> {
+ tracing::trace!("Starting job static img from disk for {url}");
+ let data = std::fs::read(path)?;
+ let image_buffer = image::load_from_memory(&data).map_err(crate::Error::Image);
+
+ let image_buffer = match image_buffer {
+ Ok(i) => i,
+ Err(e) => {
+ tracing::error!("could not load img buffer");
+ return Err(e);
+ }
+ };
+
+ let img = buffer_to_color_image(
+ image_buffer.as_flat_samples_u8(),
+ image_buffer.width(),
+ image_buffer.height(),
+ );
+
+ Ok(load_texture_checked(&ctx, url, img, Default::default()))
+}
+
+async fn fetch_static_img_from_net(
+ url: String,
+ ctx: egui::Context,
+ path: PathBuf,
+ imgtype: ImageType,
+) -> JobOutput<MediaJobResult> {
+ tracing::trace!("fetch static img from net: starting job. sending http request for {url}");
+ let res = match http_req(&url).await {
+ Ok(r) => r,
+ Err(e) => {
+ return JobOutput::complete(MediaJobResult::StaticImg(Err(crate::Error::Generic(
+ format!("Http error: {e}"),
+ ))));
+ }
+ };
+
+ tracing::trace!("static img from net: parsing http request from {url}");
+ JobOutput::Next(JobRun::Sync(Box::new(move || {
+ let img = match parse_img_response(res, imgtype) {
+ Ok(i) => i,
+ Err(e) => {
+ return JobOutput::Complete(CompleteResponse::new(MediaJobResult::StaticImg(Err(
+ e,
+ ))))
+ }
+ };
+
+ let texture_handle =
+ load_texture_checked(&ctx, url.clone(), img.clone(), Default::default());
+
+ JobOutput::Complete(
+ CompleteResponse::new(MediaJobResult::StaticImg(Ok(texture_handle))).run_no_output(
+ NoOutputRun::Sync(Box::new(move || {
+ tracing::trace!("static img from net: Saving output from {url}");
+ if let Err(e) = MediaCache::write(&path, &url, img) {
+ tracing::error!("{e}");
+ }
+ })),
+ ),
+ )
+ })))
+}
diff --git a/crates/notedeck/src/note/mod.rs b/crates/notedeck/src/note/mod.rs
@@ -4,9 +4,9 @@ mod context;
pub use action::{NoteAction, ReactAction, ScrollInfo, ZapAction, ZapTargetAmount};
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
+use crate::jobs::MediaJobSender;
use crate::Accounts;
use crate::GlobalWallet;
-use crate::JobPool;
use crate::Localization;
use crate::UnknownIds;
use crate::{notecache::NoteCache, zaps::Zaps, Images};
@@ -27,7 +27,7 @@ pub struct NoteContext<'d> {
pub note_cache: &'d mut NoteCache,
pub zaps: &'d mut Zaps,
pub pool: &'d mut RelayPool,
- pub job_pool: &'d mut JobPool,
+ pub jobs: &'d MediaJobSender,
pub unknown_ids: &'d mut UnknownIds,
pub clipboard: &'d mut egui_winit::clipboard::Clipboard,
}
diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs
@@ -484,6 +484,7 @@ fn chrome_handle_app_action(
ctx.zaps,
ctx.img_cache,
&mut columns.view_state,
+ ctx.media_jobs.sender(),
ui,
);
@@ -540,6 +541,7 @@ fn columns_route_to_profile(
ctx.zaps,
ctx.img_cache,
&mut columns.view_state,
+ ctx.media_jobs.sender(),
ui,
);
@@ -589,7 +591,8 @@ fn topdown_sidebar(
get_profile_url_owned(None)
};
- let pfp_resp = ui.add(&mut ProfilePic::new(ctx.img_cache, profile_url).size(64.0));
+ let pfp_resp = ui
+ .add(&mut ProfilePic::new(ctx.img_cache, ctx.media_jobs.sender(), profile_url).size(64.0));
ui.horizontal_wrapped(|ui| {
ui.add(egui::Label::new(
diff --git a/crates/notedeck_clndash/src/ui.rs b/crates/notedeck_clndash/src/ui.rs
@@ -49,23 +49,26 @@ pub fn note_hover_ui(
note_cache: ctx.note_cache,
zaps: ctx.zaps,
pool: ctx.pool,
- job_pool: ctx.job_pool,
+ jobs: ctx.media_jobs.sender(),
unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard,
i18n: ctx.i18n,
global_wallet: ctx.global_wallet,
};
- let mut jobs = notedeck::JobsCache::default();
let options = notedeck_ui::NoteOptions::default();
- notedeck_ui::ProfilePic::from_profile_or_default(note_context.img_cache, author.as_ref())
- .ui(ui);
+ notedeck_ui::ProfilePic::from_profile_or_default(
+ note_context.img_cache,
+ note_context.jobs,
+ author.as_ref(),
+ )
+ .ui(ui);
let nostr_name = notedeck::name::get_display_name(author.as_ref());
ui.label(format!("{} zapped you", nostr_name.name()));
- return notedeck_ui::NoteView::new(&mut note_context, ¬e, options, &mut jobs)
+ return notedeck_ui::NoteView::new(&mut note_context, ¬e, options)
.preview_style()
.hide_media(true)
.show(ui)
diff --git a/crates/notedeck_columns/src/accounts/mod.rs b/crates/notedeck_columns/src/accounts/mod.rs
@@ -1,7 +1,7 @@
use enostr::{FullKeypair, Pubkey};
use nostrdb::{Ndb, Transaction};
-use notedeck::{Accounts, AppContext, JobsCache, Localization, SingleUnkIdAction, UnknownIds};
+use notedeck::{Accounts, AppContext, Localization, SingleUnkIdAction, UnknownIds};
use notedeck_ui::nip51_set::Nip51SetUiCache;
pub use crate::accounts::route::AccountsResponse;
@@ -77,7 +77,6 @@ pub struct AddAccountAction {
pub fn render_accounts_route(
ui: &mut egui::Ui,
app_ctx: &mut AppContext,
- jobs: &mut JobsCache,
login_state: &mut AcquireKeyState,
onboarding: &mut Onboarding,
follow_packs_ui: &mut Nip51SetUiCache,
@@ -87,6 +86,7 @@ pub fn render_accounts_route(
AccountsRoute::Accounts => AccountsView::new(
app_ctx.ndb,
app_ctx.accounts,
+ app_ctx.media_jobs.sender(),
app_ctx.img_cache,
app_ctx.i18n,
)
@@ -107,8 +107,7 @@ pub fn render_accounts_route(
app_ctx.ndb,
app_ctx.img_cache,
app_ctx.i18n,
- app_ctx.job_pool,
- jobs,
+ app_ctx.media_jobs.sender(),
)
.ui(ui)
.map_output(|r| match r {
diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs
@@ -17,8 +17,8 @@ use nostrdb::{IngestMetadata, Ndb, NoteBuilder, NoteKey, Transaction};
use notedeck::{
get_wallet_for,
note::{reaction_sent_id, ReactAction, ZapTargetAmount},
- Accounts, GlobalWallet, Images, NoteAction, NoteCache, NoteZapTargetOwned, UnknownIds,
- ZapAction, ZapTarget, ZappingError, Zaps,
+ Accounts, GlobalWallet, Images, MediaJobSender, NoteAction, NoteCache, NoteZapTargetOwned,
+ UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
};
use notedeck_ui::media::MediaViewerFlags;
use tracing::error;
@@ -59,6 +59,7 @@ fn execute_note_action(
images: &mut Images,
view_state: &mut ViewState,
router_type: RouterType,
+ jobs: &MediaJobSender,
ui: &mut egui::Ui,
col: usize,
) -> NoteActionResponse {
@@ -207,7 +208,7 @@ fn execute_note_action(
.set(MediaViewerFlags::Open, true);
});
- media_action.process_default_media_actions(images)
+ media_action.process_default_media_actions(images, jobs, ui.ctx())
}
}
@@ -235,6 +236,7 @@ pub fn execute_and_process_note_action(
zaps: &mut Zaps,
images: &mut Images,
view_state: &mut ViewState,
+ jobs: &MediaJobSender,
ui: &mut egui::Ui,
) -> Option<RouterAction> {
let router_type = {
@@ -261,6 +263,7 @@ pub fn execute_and_process_note_action(
images,
view_state,
router_type,
+ jobs,
ui,
col,
);
diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs
@@ -21,7 +21,8 @@ use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPo
use nostrdb::Transaction;
use notedeck::{
tr, ui::is_narrow, Accounts, AppAction, AppContext, AppResponse, DataPath, DataPathType,
- FilterState, Images, JobsCache, Localization, NotedeckOptions, SettingsHandler, UnknownIds,
+ FilterState, Images, Localization, MediaJobSender, NotedeckOptions, SettingsHandler,
+ UnknownIds,
};
use notedeck_ui::{
media::{MediaViewer, MediaViewerFlags, MediaViewerState},
@@ -48,7 +49,6 @@ pub struct Damus {
pub timeline_cache: TimelineCache,
pub subscriptions: Subscriptions,
pub support: Support,
- pub jobs: JobsCache,
pub threads: Threads,
//frame_history: crate::frame_history::FrameHistory,
@@ -442,7 +442,12 @@ fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::
render_damus_desktop(damus, app_ctx, ui)
};
- fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
+ fullscreen_media_viewer_ui(
+ ui,
+ &mut damus.view_state.media_viewer,
+ app_ctx.img_cache,
+ app_ctx.media_jobs.sender(),
+ );
// We use this for keeping timestamps and things up to date
//ui.ctx().request_repaint_after(Duration::from_secs(5));
@@ -457,6 +462,7 @@ fn fullscreen_media_viewer_ui(
ui: &mut egui::Ui,
state: &mut MediaViewerState,
img_cache: &mut Images,
+ jobs: &MediaJobSender,
) {
if !state.should_show(ui) {
if state.scene_rect.is_some() {
@@ -468,7 +474,9 @@ fn fullscreen_media_viewer_ui(
return;
}
- let resp = MediaViewer::new(state).fullscreen(true).ui(img_cache, ui);
+ let resp = MediaViewer::new(state)
+ .fullscreen(true)
+ .ui(img_cache, jobs, ui);
if resp.clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
fullscreen_media_close(state);
@@ -568,7 +576,6 @@ impl Damus {
let support = Support::new(app_context.path);
let note_options = get_note_options(parsed_args, app_context.settings);
- let jobs = JobsCache::default();
let threads = Threads::default();
Self {
@@ -583,7 +590,6 @@ impl Damus {
support,
decks_cache,
unrecognized_args,
- jobs,
threads,
onboarding: Onboarding::default(),
hovered_column: None,
@@ -635,7 +641,6 @@ impl Damus {
options,
decks_cache,
unrecognized_args: BTreeSet::default(),
- jobs: JobsCache::default(),
threads: Threads::default(),
onboarding: Onboarding::default(),
hovered_column: None,
@@ -915,6 +920,7 @@ fn timelines_view(
ctx.i18n,
ctx.ndb,
ctx.img_cache,
+ ctx.media_jobs.sender(),
)
.show(ui);
diff --git a/crates/notedeck_columns/src/media_upload.rs b/crates/notedeck_columns/src/media_upload.rs
@@ -352,7 +352,6 @@ mod tests {
// just a random image to test image upload
let file_path = PathBuf::from_str("../../../assets/damus_rounded_80.png").unwrap();
let selected_media = SelectedMedia::from_path(file_path).unwrap();
- let img_bytes = include_bytes!("../../../assets/damus_rounded_80.png");
let promise = get_upload_url_from_provider(NOSTR_BUILD_URL());
let kp = FullKeypair::generate();
println!("Using pubkey: {:?}", kp.pubkey);
diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs
@@ -551,6 +551,7 @@ fn process_render_nav_action(
ctx.zaps,
ctx.img_cache,
&mut app.view_state,
+ ctx.media_jobs.sender(),
ui,
)
}
@@ -627,7 +628,7 @@ fn render_nav_body(
note_cache: ctx.note_cache,
zaps: ctx.zaps,
pool: ctx.pool,
- job_pool: ctx.job_pool,
+ jobs: ctx.media_jobs.sender(),
unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard,
i18n: ctx.i18n,
@@ -650,7 +651,6 @@ fn render_nav_body(
depth,
ui,
&mut note_context,
- &mut app.jobs,
scroll_to_top,
);
@@ -670,13 +670,11 @@ fn render_nav_body(
app.note_options,
ui,
&mut note_context,
- &mut app.jobs,
),
Route::Accounts(amr) => {
let resp = render_accounts_route(
ui,
ctx,
- &mut app.jobs,
&mut app.view_state.login,
&mut app.onboarding,
&mut app.view_state.follow_packs,
@@ -706,7 +704,6 @@ fn render_nav_body(
ctx.settings.get_settings_mut(),
&mut note_context,
&mut app.note_options,
- &mut app.jobs,
)
.ui(ui)
.map_output(RenderNavAction::SettingsAction),
@@ -751,7 +748,6 @@ fn render_nav_body(
¬e,
inner_rect,
options,
- &mut app.jobs,
col,
)
.show(ui)
@@ -786,7 +782,6 @@ fn render_nav_body(
¬e,
inner_rect,
app.note_options,
- &mut app.jobs,
col,
)
.show(ui);
@@ -818,7 +813,6 @@ fn render_nav_body(
kp,
inner_rect,
app.note_options,
- &mut app.jobs,
)
.ui(&txn, ui);
@@ -852,15 +846,9 @@ fn render_nav_body(
search_buffer.focus_state = FocusState::ShouldRequestFocus;
}
- SearchView::new(
- &txn,
- app.note_options,
- search_buffer,
- &mut note_context,
- &mut app.jobs,
- )
- .show(ui)
- .map_output(RenderNavAction::NoteAction)
+ SearchView::new(&txn, app.note_options, search_buffer, &mut note_context)
+ .show(ui)
+ .map_output(RenderNavAction::NoteAction)
}
Route::NewDeck => {
let id = ui.id().with("new-deck");
@@ -933,22 +921,28 @@ fn render_nav_body(
return BodyResponse::none();
};
- EditProfileView::new(ctx.i18n, state, ctx.img_cache, ctx.clipboard)
- .ui(ui)
- .map_output_maybe(|save| {
- if save {
- app.view_state
- .pubkey_to_profile_state
- .get(kp.pubkey)
- .map(|state| {
- RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
- SaveProfileChanges::new(kp.to_full(), state.clone()),
- ))
- })
- } else {
- None
- }
- })
+ EditProfileView::new(
+ ctx.i18n,
+ state,
+ ctx.img_cache,
+ ctx.clipboard,
+ ctx.media_jobs.sender(),
+ )
+ .ui(ui)
+ .map_output_maybe(|save| {
+ if save {
+ app.view_state
+ .pubkey_to_profile_state
+ .get(kp.pubkey)
+ .map(|state| {
+ RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
+ SaveProfileChanges::new(kp.to_full(), state.clone()),
+ ))
+ })
+ } else {
+ None
+ }
+ })
}
Route::Following(pubkey) => {
let cache_id = egui::Id::new(("following_contacts_cache", pubkey));
@@ -1068,6 +1062,7 @@ fn render_nav_body(
&txn,
&target.zap_recipient,
default_msats,
+ ctx.media_jobs.sender(),
)
.ui(ui),
)
@@ -1218,6 +1213,7 @@ pub fn render_nav(
std::slice::from_ref(route),
col,
ctx.i18n,
+ ctx.media_jobs.sender(),
)
.show_move_button(!narrow)
.show_delete_button(!narrow)
@@ -1262,6 +1258,7 @@ pub fn render_nav(
nav.routes(),
col,
ctx.i18n,
+ ctx.media_jobs.sender(),
)
.show_move_button(!narrow)
.show_delete_button(!narrow)
diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs
@@ -6,7 +6,7 @@ use crate::{
};
use enostr::Pubkey;
-use notedeck::{JobsCache, NoteContext};
+use notedeck::NoteContext;
use notedeck_ui::NoteOptions;
#[allow(clippy::too_many_arguments)]
@@ -18,7 +18,6 @@ pub fn render_timeline_route(
depth: usize,
ui: &mut egui::Ui,
note_context: &mut NoteContext,
- jobs: &mut JobsCache,
scroll_to_top: bool,
) -> BodyResponse<RenderNavAction> {
match kind {
@@ -30,35 +29,20 @@ pub fn render_timeline_route(
| TimelineKind::Hashtag(_)
| TimelineKind::Generic(_) => {
let resp =
- ui::TimelineView::new(kind, timeline_cache, note_context, note_options, jobs, col)
- .ui(ui);
+ ui::TimelineView::new(kind, timeline_cache, note_context, note_options, col).ui(ui);
resp.map_output(RenderNavAction::NoteAction)
}
TimelineKind::Profile(pubkey) => {
if depth > 1 {
- render_profile_route(
- pubkey,
- timeline_cache,
- col,
- ui,
- note_options,
- note_context,
- jobs,
- )
+ render_profile_route(pubkey, timeline_cache, col, ui, note_options, note_context)
} else {
// we render profiles like timelines if they are at the root
- let resp = ui::TimelineView::new(
- kind,
- timeline_cache,
- note_context,
- note_options,
- jobs,
- col,
- )
- .scroll_to_top(scroll_to_top)
- .ui(ui);
+ let resp =
+ ui::TimelineView::new(kind, timeline_cache, note_context, note_options, col)
+ .scroll_to_top(scroll_to_top)
+ .ui(ui);
resp.map_output(RenderNavAction::NoteAction)
}
@@ -74,7 +58,6 @@ pub fn render_thread_route(
mut note_options: NoteOptions,
ui: &mut egui::Ui,
note_context: &mut NoteContext,
- jobs: &mut JobsCache,
) -> BodyResponse<RenderNavAction> {
// don't truncate thread notes for now, since they are
// default truncated everywher eelse
@@ -88,7 +71,6 @@ pub fn render_thread_route(
selection.selected_or_root(),
note_options,
note_context,
- jobs,
col,
)
.ui(ui)
@@ -103,17 +85,9 @@ pub fn render_profile_route(
ui: &mut egui::Ui,
note_options: NoteOptions,
note_context: &mut NoteContext,
- jobs: &mut JobsCache,
) -> BodyResponse<RenderNavAction> {
- let profile_view = ProfileView::new(
- pubkey,
- col,
- timeline_cache,
- note_options,
- note_context,
- jobs,
- )
- .ui(ui);
+ let profile_view =
+ ProfileView::new(pubkey, col, timeline_cache, note_options, note_context).ui(ui);
profile_view.map_output_maybe(|action| match action {
ui::profile::ProfileViewAction::EditProfile => note_context
diff --git a/crates/notedeck_columns/src/ui/accounts.rs b/crates/notedeck_columns/src/ui/accounts.rs
@@ -3,7 +3,7 @@ use egui::{
};
use enostr::Pubkey;
use nostrdb::{Ndb, Transaction};
-use notedeck::{tr, Accounts, Images, Localization};
+use notedeck::{tr, Accounts, Images, Localization, MediaJobSender};
use notedeck_ui::colors::PINK;
use notedeck_ui::profile::preview::SimpleProfilePreview;
@@ -15,6 +15,7 @@ pub struct AccountsView<'a> {
ndb: &'a Ndb,
accounts: &'a Accounts,
img_cache: &'a mut Images,
+ jobs: &'a MediaJobSender,
i18n: &'a mut Localization,
}
@@ -35,6 +36,7 @@ impl<'a> AccountsView<'a> {
pub fn new(
ndb: &'a Ndb,
accounts: &'a Accounts,
+ jobs: &'a MediaJobSender,
img_cache: &'a mut Images,
i18n: &'a mut Localization,
) -> Self {
@@ -43,6 +45,7 @@ impl<'a> AccountsView<'a> {
accounts,
img_cache,
i18n,
+ jobs,
}
}
@@ -57,7 +60,14 @@ impl<'a> AccountsView<'a> {
let scroll_out = scroll_area()
.id_salt(AccountsView::scroll_id())
.show(ui, |ui| {
- Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache, self.i18n)
+ Self::show_accounts(
+ ui,
+ self.accounts,
+ self.ndb,
+ self.img_cache,
+ self.jobs,
+ self.i18n,
+ )
});
out.set_scroll_id(&scroll_out);
@@ -77,6 +87,7 @@ impl<'a> AccountsView<'a> {
accounts: &Accounts,
ndb: &Ndb,
img_cache: &mut Images,
+ jobs: &MediaJobSender,
i18n: &mut Localization,
) -> Option<AccountsViewResponse> {
let mut return_op: Option<AccountsViewResponse> = None;
@@ -103,6 +114,7 @@ impl<'a> AccountsView<'a> {
let preview = SimpleProfilePreview::new(
profile.as_ref(),
img_cache,
+ jobs,
i18n,
has_nsec,
);
diff --git a/crates/notedeck_columns/src/ui/add_column.rs b/crates/notedeck_columns/src/ui/add_column.rs
@@ -17,7 +17,9 @@ use crate::{
Damus,
};
-use notedeck::{tr, AppContext, Images, Localization, NotedeckTextStyle, UserAccount};
+use notedeck::{
+ tr, AppContext, Images, Localization, MediaJobSender, NotedeckTextStyle, UserAccount,
+};
use notedeck_ui::{anim::ICON_EXPANSION_MULTIPLE, app_images};
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
@@ -168,6 +170,7 @@ pub struct AddColumnView<'a> {
img_cache: &'a mut Images,
cur_account: &'a UserAccount,
i18n: &'a mut Localization,
+ jobs: &'a MediaJobSender,
}
impl<'a> AddColumnView<'a> {
@@ -177,6 +180,7 @@ impl<'a> AddColumnView<'a> {
img_cache: &'a mut Images,
cur_account: &'a UserAccount,
i18n: &'a mut Localization,
+ jobs: &'a MediaJobSender,
) -> Self {
Self {
key_state_map,
@@ -184,6 +188,7 @@ impl<'a> AddColumnView<'a> {
img_cache,
cur_account,
i18n,
+ jobs,
}
}
@@ -349,7 +354,7 @@ impl<'a> AddColumnView<'a> {
bottom: 32,
})
.show(ui, |ui| {
- ProfilePreview::new(&profile, self.img_cache).ui(ui);
+ ProfilePreview::new(&profile, self.img_cache, self.jobs).ui(ui);
});
}
}
@@ -673,6 +678,7 @@ pub fn render_add_column_routes(
ctx.img_cache,
ctx.accounts.get_selected_account(),
ctx.i18n,
+ ctx.media_jobs.sender(),
);
let resp = match route {
AddColumnRoute::Base => add_column_view.ui(ui),
diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs
@@ -13,7 +13,7 @@ use egui::{Margin, Response, RichText, Sense, Stroke, UiBuilder};
use enostr::Pubkey;
use nostrdb::{Ndb, Transaction};
use notedeck::tr;
-use notedeck::{Images, Localization, NotedeckTextStyle};
+use notedeck::{Images, Localization, MediaJobSender, NotedeckTextStyle};
use notedeck_ui::app_images;
use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
@@ -28,6 +28,7 @@ pub struct NavTitle<'a> {
col_id: usize,
options: u32,
i18n: &'a mut Localization,
+ jobs: &'a MediaJobSender,
}
impl<'a> NavTitle<'a> {
@@ -42,6 +43,7 @@ impl<'a> NavTitle<'a> {
routes: &'a [Route],
col_id: usize,
i18n: &'a mut Localization,
+ jobs: &'a MediaJobSender,
) -> Self {
let options = Self::SHOW_MOVE | Self::SHOW_DELETE;
NavTitle {
@@ -52,6 +54,7 @@ impl<'a> NavTitle<'a> {
col_id,
options,
i18n,
+ jobs,
}
}
@@ -435,11 +438,8 @@ impl<'a> NavTitle<'a> {
.as_ref()
.ok()
.and_then(move |p| {
- Some(
- ProfilePic::from_profile(self.img_cache, p)?
- .size(pfp_size)
- .sense(Sense::click()),
- )
+ ProfilePic::from_profile(self.img_cache, self.jobs, p)
+ .map(|pfp| pfp.size(pfp_size).sense(Sense::click()))
})
}
@@ -453,7 +453,7 @@ impl<'a> NavTitle<'a> {
ui.add(&mut pfp)
} else {
ui.add(
- &mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url())
+ &mut ProfilePic::new(self.img_cache, self.jobs, notedeck::profile::no_pfp_url())
.size(pfp_size)
.sense(Sense::click()),
)
@@ -515,7 +515,7 @@ impl<'a> NavTitle<'a> {
ui.add(&mut pfp)
} else {
ui.add(
- &mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url())
+ &mut ProfilePic::new(self.img_cache, self.jobs, notedeck::profile::no_pfp_url())
.size(pfp_size)
.sense(Sense::click()),
)
@@ -536,7 +536,10 @@ impl<'a> NavTitle<'a> {
}
}
- ui.add(&mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()).size(pfp_size))
+ ui.add(
+ &mut ProfilePic::new(self.img_cache, self.jobs, notedeck::profile::no_pfp_url())
+ .size(pfp_size),
+ )
}
fn title_label_value(title: &str) -> egui::Label {
diff --git a/crates/notedeck_columns/src/ui/mentions_picker.rs b/crates/notedeck_columns/src/ui/mentions_picker.rs
@@ -1,7 +1,7 @@
use egui::{vec2, FontId, Layout, Pos2, Rect, ScrollArea, UiBuilder, Vec2b};
use nostrdb::{Ndb, ProfileRecord, Transaction};
use notedeck::{
- fonts::get_font_size, name::get_display_name, profile::get_profile_url, Images,
+ fonts::get_font_size, name::get_display_name, profile::get_profile_url, Images, MediaJobSender,
NotedeckTextStyle,
};
use notedeck_ui::{
@@ -20,6 +20,7 @@ pub struct MentionPickerView<'a> {
txn: &'a Transaction,
img_cache: &'a mut Images,
results: &'a Vec<&'a [u8; 32]>,
+ jobs: &'a MediaJobSender,
}
pub enum MentionPickerResponse {
@@ -33,12 +34,14 @@ impl<'a> MentionPickerView<'a> {
ndb: &'a Ndb,
txn: &'a Transaction,
results: &'a Vec<&'a [u8; 32]>,
+ jobs: &'a MediaJobSender,
) -> Self {
Self {
ndb,
txn,
img_cache,
results,
+ jobs,
}
}
@@ -55,7 +58,7 @@ impl<'a> MentionPickerView<'a> {
};
if ui
- .add(user_result(&profile, self.img_cache, i, width))
+ .add(user_result(&profile, self.img_cache, self.jobs, i, width))
.clicked()
{
selection = Some(i)
@@ -129,6 +132,7 @@ impl<'a> MentionPickerView<'a> {
fn user_result<'a>(
profile: &'a ProfileRecord<'_>,
cache: &'a mut Images,
+ jobs: &'a MediaJobSender,
index: usize,
width: f32,
) -> impl egui::Widget + 'a {
@@ -161,7 +165,7 @@ fn user_result<'a>(
let pfp_resp = ui.put(
icon_rect,
- &mut ProfilePic::new(cache, get_profile_url(Some(profile)))
+ &mut ProfilePic::new(cache, jobs, get_profile_url(Some(profile)))
.size(helper.scale_1d_pos(min_img_size)),
);
diff --git a/crates/notedeck_columns/src/ui/note/custom_zap.rs b/crates/notedeck_columns/src/ui/note/custom_zap.rs
@@ -6,7 +6,7 @@ use enostr::Pubkey;
use nostrdb::{Ndb, ProfileRecord, Transaction};
use notedeck::{
fonts::get_font_size, get_profile_url, name::get_display_name, tr, Images, Localization,
- NotedeckTextStyle,
+ MediaJobSender, NotedeckTextStyle,
};
use notedeck_ui::{
app_images, colors, profile::display_name_widget, widgets::styled_button_toggleable,
@@ -20,6 +20,7 @@ pub struct CustomZapView<'a> {
target_pubkey: &'a Pubkey,
default_msats: u64,
i18n: &'a mut Localization,
+ jobs: &'a MediaJobSender,
}
#[allow(clippy::new_without_default)]
@@ -31,6 +32,7 @@ impl<'a> CustomZapView<'a> {
txn: &'a Transaction,
target_pubkey: &'a Pubkey,
default_msats: u64,
+ jobs: &'a MediaJobSender,
) -> Self {
Self {
target_pubkey,
@@ -39,6 +41,7 @@ impl<'a> CustomZapView<'a> {
txn,
default_msats,
i18n,
+ jobs,
}
}
@@ -59,7 +62,7 @@ impl<'a> CustomZapView<'a> {
.get_profile_by_pubkey(self.txn, self.target_pubkey.bytes())
.ok();
let profile = profile.as_ref();
- show_profile(ui, self.images, profile);
+ show_profile(ui, self.images, self.jobs, profile);
ui.add_space(8.0);
@@ -167,13 +170,18 @@ fn show_title(ui: &mut egui::Ui, i18n: &mut Localization) {
);
}
-fn show_profile(ui: &mut egui::Ui, images: &mut Images, profile: Option<&ProfileRecord>) {
+fn show_profile(
+ ui: &mut egui::Ui,
+ images: &mut Images,
+ jobs: &MediaJobSender,
+ profile: Option<&ProfileRecord>,
+) {
let max_size = 24.0;
ui.allocate_ui_with_layout(
vec2(ui.available_width(), max_size),
Layout::left_to_right(egui::Align::Center).with_main_wrap(true),
|ui| {
- ui.add(&mut ProfilePic::new(images, get_profile_url(profile)).size(max_size));
+ ui.add(&mut ProfilePic::new(images, jobs, get_profile_url(profile)).size(max_size));
ui.vertical(|ui| {
ui.add(display_name_widget(&get_display_name(profile), false));
});
diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs
@@ -13,12 +13,12 @@ use egui::{
};
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
use nostrdb::{Ndb, Transaction};
-use notedeck::media::gif::ensure_latest_texture;
+use notedeck::media::latest::LatestImageTex;
use notedeck::media::AnimationMode;
#[cfg(target_os = "android")]
use notedeck::platform::android::try_open_file_picker;
use notedeck::platform::get_next_selected_file;
-use notedeck::{get_render_state, JobsCache, PixelDimensions, RenderState};
+use notedeck::PixelDimensions;
use notedeck::{
name::get_display_name, supported_mime_hosted_at_url, tr, Localization, NoteAction, NoteContext,
};
@@ -39,7 +39,6 @@ pub struct PostView<'a, 'd> {
poster: FilledKeypair<'a>,
inner_rect: egui::Rect,
note_options: NoteOptions,
- jobs: &'a mut JobsCache,
animation_mode: AnimationMode,
}
@@ -112,7 +111,6 @@ impl<'a, 'd> PostView<'a, 'd> {
poster: FilledKeypair<'a>,
inner_rect: egui::Rect,
note_options: NoteOptions,
- jobs: &'a mut JobsCache,
) -> Self {
let animation_mode = if note_options.contains(NoteOptions::NoAnimations) {
AnimationMode::NoAnimation
@@ -127,7 +125,6 @@ impl<'a, 'd> PostView<'a, 'd> {
inner_rect,
note_options,
animation_mode,
- jobs,
}
}
@@ -157,15 +154,20 @@ impl<'a, 'd> PostView<'a, 'd> {
.as_ref()
.ok()
.and_then(|p| {
- Some(ProfilePic::from_profile(self.note_context.img_cache, p)?.size(pfp_size))
+ ProfilePic::from_profile(self.note_context.img_cache, self.note_context.jobs, p)
+ .map(|pfp| pfp.size(pfp_size))
});
if let Some(mut pfp) = poster_pfp {
ui.add(&mut pfp);
} else {
ui.add(
- &mut ProfilePic::new(self.note_context.img_cache, notedeck::profile::no_pfp_url())
- .size(pfp_size),
+ &mut ProfilePic::new(
+ self.note_context.img_cache,
+ self.note_context.jobs,
+ notedeck::profile::no_pfp_url(),
+ )
+ .size(pfp_size),
);
}
@@ -307,6 +309,7 @@ impl<'a, 'd> PostView<'a, 'd> {
self.note_context.ndb,
txn,
&res,
+ self.note_context.jobs,
)
.show_in_rect(hint_rect, ui);
@@ -443,7 +446,6 @@ impl<'a, 'd> PostView<'a, 'd> {
id.bytes(),
nostrdb::NoteKey::new(0),
self.note_options,
- self.jobs,
)
})
.inner
@@ -541,13 +543,19 @@ impl<'a, 'd> PostView<'a, 'd> {
};
let url = &media.url;
- let cur_state = get_render_state(
- ui.ctx(),
- self.note_context.img_cache,
- cache_type,
- url,
- notedeck::ImageType::Content(Some((width, height))),
- );
+
+ let cur_state = self
+ .note_context
+ .img_cache
+ .no_img_loading_tex_loader()
+ .latest_state(
+ self.note_context.jobs,
+ ui.ctx(),
+ url,
+ cache_type,
+ notedeck::ImageType::Content(Some((width, height))),
+ self.animation_mode,
+ );
render_post_view_media(
ui,
@@ -557,8 +565,6 @@ impl<'a, 'd> PostView<'a, 'd> {
width,
height,
cur_state,
- url,
- self.animation_mode,
)
}
to_remove.reverse();
@@ -644,19 +650,17 @@ fn render_post_view_media(
cur_index: usize,
width: u32,
height: u32,
- render_state: RenderState,
- url: &str,
- animation_mode: AnimationMode,
+ render_state: LatestImageTex,
) {
- match render_state.texture_state {
- notedeck::TextureState::Pending => {
+ match render_state {
+ LatestImageTex::Pending => {
ui.spinner();
}
- notedeck::TextureState::Error(e) => {
+ LatestImageTex::Error(e) => {
upload_errors.push(e.to_string());
error!("{e}");
}
- notedeck::TextureState::Loaded(renderable_media) => {
+ LatestImageTex::Loaded(tex) => {
let max_size = 300;
let size = if width > max_size || height > max_size {
PixelDimensions { x: 300, y: 300 }
@@ -669,13 +673,7 @@ fn render_post_view_media(
.to_points(ui.pixels_per_point())
.to_vec();
- let texture_handle =
- ensure_latest_texture(ui, url, render_state.gifs, renderable_media, animation_mode);
- let img_resp = ui.add(
- egui::Image::new(&texture_handle)
- .max_size(size)
- .corner_radius(12.0),
- );
+ let img_resp = ui.add(egui::Image::new(tex).max_size(size).corner_radius(12.0));
let remove_button_rect = {
let top_left = img_resp.rect.left_top();
@@ -837,7 +835,6 @@ mod preview {
pub struct PostPreview {
draft: Draft,
poster: FullKeypair,
- jobs: JobsCache,
}
impl PostPreview {
@@ -867,7 +864,6 @@ mod preview {
PostPreview {
draft,
poster: FullKeypair::generate(),
- jobs: Default::default(),
}
}
}
@@ -883,7 +879,7 @@ mod preview {
note_cache: app.note_cache,
zaps: app.zaps,
pool: app.pool,
- job_pool: app.job_pool,
+ jobs: app.media_jobs.sender(),
unknown_ids: app.unknown_ids,
clipboard: app.clipboard,
i18n: app.i18n,
@@ -896,7 +892,6 @@ mod preview {
self.poster.to_filled(),
ui.available_rect_before_wrap(),
NoteOptions::default(),
- &mut self.jobs,
)
.ui(&txn, ui);
diff --git a/crates/notedeck_columns/src/ui/note/quote_repost.rs b/crates/notedeck_columns/src/ui/note/quote_repost.rs
@@ -7,7 +7,7 @@ use crate::{
use egui::ScrollArea;
use enostr::{FilledKeypair, NoteId};
-use notedeck::{JobsCache, NoteContext};
+use notedeck::NoteContext;
use notedeck_ui::NoteOptions;
pub struct QuoteRepostView<'a, 'd> {
@@ -18,7 +18,6 @@ pub struct QuoteRepostView<'a, 'd> {
scroll_id: egui::Id,
inner_rect: egui::Rect,
note_options: NoteOptions,
- jobs: &'a mut JobsCache,
}
impl<'a, 'd> QuoteRepostView<'a, 'd> {
@@ -30,7 +29,6 @@ impl<'a, 'd> QuoteRepostView<'a, 'd> {
quoting_note: &'a nostrdb::Note<'a>,
inner_rect: egui::Rect,
note_options: NoteOptions,
- jobs: &'a mut JobsCache,
col: usize,
) -> Self {
QuoteRepostView {
@@ -41,7 +39,6 @@ impl<'a, 'd> QuoteRepostView<'a, 'd> {
scroll_id: QuoteRepostView::scroll_id(col, quoting_note.id()),
inner_rect,
note_options,
- jobs,
}
}
@@ -78,7 +75,6 @@ impl<'a, 'd> QuoteRepostView<'a, 'd> {
self.poster,
self.inner_rect,
self.note_options,
- self.jobs,
)
.ui_no_scroll(self.quoting_note.txn().unwrap(), ui);
post_resp
diff --git a/crates/notedeck_columns/src/ui/note/reply.rs b/crates/notedeck_columns/src/ui/note/reply.rs
@@ -7,7 +7,7 @@ use crate::ui::{
use egui::{Rect, Response, ScrollArea, Ui};
use enostr::{FilledKeypair, NoteId};
-use notedeck::{JobsCache, NoteContext};
+use notedeck::NoteContext;
use notedeck_ui::{NoteOptions, NoteView, ProfilePic};
pub struct PostReplyView<'a, 'd> {
@@ -18,7 +18,6 @@ pub struct PostReplyView<'a, 'd> {
scroll_id: egui::Id,
inner_rect: egui::Rect,
note_options: NoteOptions,
- jobs: &'a mut JobsCache,
}
impl<'a, 'd> PostReplyView<'a, 'd> {
@@ -30,7 +29,6 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
note: &'a nostrdb::Note<'a>,
inner_rect: egui::Rect,
note_options: NoteOptions,
- jobs: &'a mut JobsCache,
col: usize,
) -> Self {
PostReplyView {
@@ -41,7 +39,6 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
scroll_id: PostReplyView::scroll_id(col, note.id()),
inner_rect,
note_options,
- jobs,
}
}
@@ -85,7 +82,7 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
let quoted_note = egui::Frame::NONE
.outer_margin(egui::Margin::same(note_offset))
.show(ui, |ui| {
- NoteView::new(self.note_context, self.note, self.note_options, self.jobs)
+ NoteView::new(self.note_context, self.note, self.note_options)
.truncate(false)
.selectable_text(true)
.actionbar(false)
@@ -106,7 +103,6 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
self.poster,
self.inner_rect,
self.note_options,
- self.jobs,
)
.ui_no_scroll(self.note.txn().unwrap(), ui)
};
diff --git a/crates/notedeck_columns/src/ui/onboarding.rs b/crates/notedeck_columns/src/ui/onboarding.rs
@@ -2,7 +2,7 @@ use std::mem;
use egui::{Layout, ScrollArea};
use nostrdb::Ndb;
-use notedeck::{tr, Images, JobPool, JobsCache, Localization};
+use notedeck::{tr, Images, Localization, MediaJobSender};
use notedeck_ui::{
colors,
nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetAction, Nip51SetWidgetFlags},
@@ -17,8 +17,7 @@ pub struct FollowPackOnboardingView<'a> {
ndb: &'a Ndb,
images: &'a mut Images,
loc: &'a mut Localization,
- job_pool: &'a mut JobPool,
- jobs: &'a mut JobsCache,
+ jobs: &'a MediaJobSender,
}
pub enum OnboardingResponse {
@@ -38,8 +37,7 @@ impl<'a> FollowPackOnboardingView<'a> {
ndb: &'a Ndb,
images: &'a mut Images,
loc: &'a mut Localization,
- job_pool: &'a mut JobPool,
- jobs: &'a mut JobsCache,
+ jobs: &'a MediaJobSender,
) -> Self {
Self {
onboarding,
@@ -47,7 +45,6 @@ impl<'a> FollowPackOnboardingView<'a> {
ndb,
images,
loc,
- job_pool,
jobs,
}
}
@@ -81,7 +78,6 @@ impl<'a> FollowPackOnboardingView<'a> {
self.ndb,
self.loc,
self.images,
- self.job_pool,
self.jobs,
)
.with_flags(Nip51SetWidgetFlags::TRUST_IMAGES)
diff --git a/crates/notedeck_columns/src/ui/profile/contacts_list.rs b/crates/notedeck_columns/src/ui/profile/contacts_list.rs
@@ -67,7 +67,12 @@ impl<'a, 'd, 'txn> ContactsListView<'a, 'd, 'txn> {
ui.add_space(16.0);
ui.add(
- &mut ProfilePic::new(self.note_context.img_cache, profile_url).size(48.0),
+ &mut ProfilePic::new(
+ self.note_context.img_cache,
+ self.note_context.jobs,
+ profile_url,
+ )
+ .size(48.0),
);
ui.add_space(12.0);
diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs
@@ -3,7 +3,9 @@ use core::f32;
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
use egui_winit::clipboard::Clipboard;
use enostr::ProfileState;
-use notedeck::{profile::unwrap_profile_url, tr, Images, Localization, NotedeckTextStyle};
+use notedeck::{
+ profile::unwrap_profile_url, tr, Images, Localization, MediaJobSender, NotedeckTextStyle,
+};
use notedeck_ui::context_menu::{input_context, PasteBehavior};
use notedeck_ui::{profile::banner, ProfilePic};
@@ -14,6 +16,7 @@ pub struct EditProfileView<'a> {
clipboard: &'a mut Clipboard,
img_cache: &'a mut Images,
i18n: &'a mut Localization,
+ jobs: &'a MediaJobSender,
}
impl<'a> EditProfileView<'a> {
@@ -22,12 +25,14 @@ impl<'a> EditProfileView<'a> {
state: &'a mut ProfileState,
img_cache: &'a mut Images,
clipboard: &'a mut Clipboard,
+ jobs: &'a MediaJobSender,
) -> Self {
Self {
i18n,
state,
img_cache,
clipboard,
+ jobs,
}
}
@@ -89,7 +94,7 @@ impl<'a> EditProfileView<'a> {
let pfp_url = unwrap_profile_url(self.state.picture());
ui.put(
pfp_rect,
- &mut ProfilePic::new(self.img_cache, pfp_url)
+ &mut ProfilePic::new(self.img_cache, self.jobs, pfp_url)
.size(size)
.border(ProfilePic::border_stroke(ui)),
);
diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs
@@ -17,8 +17,8 @@ use crate::{
ui::timeline::{tabs_ui, TimelineTabView},
};
use notedeck::{
- name::get_display_name, profile::get_profile_url, IsFollowing, JobsCache, NoteAction,
- NoteContext, NotedeckTextStyle,
+ name::get_display_name, profile::get_profile_url, IsFollowing, NoteAction, NoteContext,
+ NotedeckTextStyle,
};
use notedeck_ui::{
app_images,
@@ -32,7 +32,6 @@ pub struct ProfileView<'a, 'd> {
timeline_cache: &'a mut TimelineCache,
note_options: NoteOptions,
note_context: &'a mut NoteContext<'d>,
- jobs: &'a mut JobsCache,
}
pub enum ProfileViewAction {
@@ -58,7 +57,6 @@ impl<'a, 'd> ProfileView<'a, 'd> {
timeline_cache: &'a mut TimelineCache,
note_options: NoteOptions,
note_context: &'a mut NoteContext<'d>,
- jobs: &'a mut JobsCache,
) -> Self {
ProfileView {
pubkey,
@@ -66,7 +64,6 @@ impl<'a, 'd> ProfileView<'a, 'd> {
timeline_cache,
note_options,
note_context,
- jobs,
}
}
@@ -125,7 +122,6 @@ impl<'a, 'd> ProfileView<'a, 'd> {
self.note_options,
&txn,
self.note_context,
- self.jobs,
)
.show(ui)
{
@@ -191,9 +187,13 @@ fn profile_body(
ui.horizontal(|ui| {
ui.put(
pfp_rect,
- &mut ProfilePic::new(note_context.img_cache, get_profile_url(profile))
- .size(size)
- .border(ProfilePic::border_stroke(ui)),
+ &mut ProfilePic::new(
+ note_context.img_cache,
+ note_context.jobs,
+ get_profile_url(profile),
+ )
+ .size(size)
+ .border(ProfilePic::border_stroke(ui)),
);
if ui
diff --git a/crates/notedeck_columns/src/ui/search/mod.rs b/crates/notedeck_columns/src/ui/search/mod.rs
@@ -11,7 +11,7 @@ use egui_winit::clipboard::Clipboard;
use nostrdb::{Filter, Ndb, ProfileRecord, Transaction};
use notedeck::{
fonts::get_font_size, name::get_display_name, profile::get_profile_url, tr, tr_plural, Images,
- JobsCache, Localization, NoteAction, NoteContext, NoteRef, NotedeckTextStyle,
+ Localization, MediaJobSender, NoteAction, NoteContext, NoteRef, NotedeckTextStyle,
};
use notedeck_ui::{
@@ -33,7 +33,6 @@ pub struct SearchView<'a, 'd> {
note_options: NoteOptions,
txn: &'a Transaction,
note_context: &'a mut NoteContext<'d>,
- jobs: &'a mut JobsCache,
}
impl<'a, 'd> SearchView<'a, 'd> {
@@ -42,14 +41,12 @@ impl<'a, 'd> SearchView<'a, 'd> {
note_options: NoteOptions,
query: &'a mut SearchQueryState,
note_context: &'a mut NoteContext<'d>,
- jobs: &'a mut JobsCache,
) -> Self {
Self {
txn,
query,
note_options,
note_context,
- jobs,
}
}
@@ -174,6 +171,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
self.note_context.ndb,
self.txn,
&results,
+ self.note_context.jobs,
)
.show_in_rect(ui.available_rect_before_wrap(), ui);
@@ -256,12 +254,10 @@ impl<'a, 'd> SearchView<'a, 'd> {
let resp = ui.add(recent_profile_item(
profile.as_ref(),
- &pubkey,
- &self.query.string,
is_selected,
ui.available_width(),
self.note_context.img_cache,
- self.note_context.accounts,
+ self.note_context.jobs,
));
if resp.clicked() {
@@ -319,7 +315,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
self.query.remove_recent_search(i);
}
}
- RecentSearchItem::Profile { pubkey, query } => {
+ RecentSearchItem::Profile { pubkey, query: _ } => {
let profile = self
.note_context
.ndb
@@ -327,12 +323,10 @@ impl<'a, 'd> SearchView<'a, 'd> {
.ok();
let resp = ui.add(recent_profile_item(
profile.as_ref(),
- pubkey,
- query,
is_selected,
ui.available_width(),
self.note_context.img_cache,
- self.note_context.accounts,
+ self.note_context.jobs,
));
if resp.clicked() || (is_selected && keyboard_resp.enter_pressed) {
@@ -360,7 +354,6 @@ impl<'a, 'd> SearchView<'a, 'd> {
self.note_options,
self.txn,
self.note_context,
- self.jobs,
)
.show(ui)
});
@@ -709,12 +702,10 @@ fn search_hashtag(
fn recent_profile_item<'a>(
profile: Option<&'a ProfileRecord<'_>>,
- _pubkey: &'a Pubkey,
- _query: &'a str,
is_selected: bool,
width: f32,
cache: &'a mut Images,
- _accounts: &'a notedeck::Accounts,
+ jobs: &'a MediaJobSender,
) -> impl egui::Widget + 'a {
move |ui: &mut egui::Ui| -> egui::Response {
let min_img_size = 48.0;
@@ -742,7 +733,7 @@ fn recent_profile_item<'a>(
ui.put(
pfp_rect,
- &mut ProfilePic::new(cache, get_profile_url(profile)).size(min_img_size),
+ &mut ProfilePic::new(cache, jobs, get_profile_url(profile)).size(min_img_size),
);
let name = get_display_name(profile).name();
diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs
@@ -6,7 +6,7 @@ use egui_extras::{Size, StripBuilder};
use enostr::NoteId;
use nostrdb::Transaction;
use notedeck::{
- tr, ui::richtext_small, Images, JobsCache, LanguageIdentifier, Localization, NoteContext,
+ tr, ui::richtext_small, Images, LanguageIdentifier, Localization, NoteContext,
NotedeckTextStyle, Settings, SettingsHandler, DEFAULT_MAX_HASHTAGS_PER_NOTE,
DEFAULT_NOTE_BODY_FONT_SIZE,
};
@@ -109,7 +109,6 @@ pub struct SettingsView<'a> {
settings: &'a mut Settings,
note_context: &'a mut NoteContext<'a>,
note_options: &'a mut NoteOptions,
- jobs: &'a mut JobsCache,
}
fn settings_group<S>(ui: &mut egui::Ui, title: S, contents: impl FnOnce(&mut egui::Ui))
@@ -136,13 +135,11 @@ impl<'a> SettingsView<'a> {
settings: &'a mut Settings,
note_context: &'a mut NoteContext<'a>,
note_options: &'a mut NoteOptions,
- jobs: &'a mut JobsCache,
) -> Self {
Self {
settings,
note_context,
note_options,
- jobs,
}
}
@@ -210,15 +207,10 @@ impl<'a> SettingsView<'a> {
if notedeck::ui::is_narrow(ui.ctx()) {
ui.set_max_width(ui.available_width());
- NoteView::new(
- self.note_context,
- &preview_note,
- *self.note_options,
- self.jobs,
- )
- .actionbar(false)
- .options_button(false)
- .show(ui);
+ NoteView::new(self.note_context, &preview_note, *self.note_options)
+ .actionbar(false)
+ .options_button(false)
+ .show(ui);
}
});
ui.separator();
diff --git a/crates/notedeck_columns/src/ui/side_panel.rs b/crates/notedeck_columns/src/ui/side_panel.rs
@@ -12,7 +12,7 @@ use crate::{
route::Route,
};
-use notedeck::{tr, Accounts, Localization, UserAccount};
+use notedeck::{tr, Accounts, Localization, MediaJobSender, UserAccount};
use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
app_images, colors, ProfilePic, View,
@@ -29,6 +29,7 @@ pub struct DesktopSidePanel<'a> {
i18n: &'a mut Localization,
ndb: &'a nostrdb::Ndb,
img_cache: &'a mut notedeck::Images,
+ jobs: &'a MediaJobSender,
}
impl View for DesktopSidePanel<'_> {
@@ -68,6 +69,7 @@ impl<'a> DesktopSidePanel<'a> {
i18n: &'a mut Localization,
ndb: &'a nostrdb::Ndb,
img_cache: &'a mut notedeck::Images,
+ jobs: &'a MediaJobSender,
) -> Self {
Self {
selected_account,
@@ -75,6 +77,7 @@ impl<'a> DesktopSidePanel<'a> {
i18n,
ndb,
img_cache,
+ jobs,
}
}
@@ -163,7 +166,7 @@ impl<'a> DesktopSidePanel<'a> {
let pfp_resp = ui
.add(
- &mut ProfilePic::new(self.img_cache, profile_url)
+ &mut ProfilePic::new(self.img_cache, self.jobs, profile_url)
.size(avatar_size)
.sense(egui::Sense::click()),
)
diff --git a/crates/notedeck_columns/src/ui/thread.rs b/crates/notedeck_columns/src/ui/thread.rs
@@ -2,7 +2,6 @@ use egui::InnerResponse;
use egui_virtual_list::VirtualList;
use nostrdb::{Note, Transaction};
use notedeck::note::root_note_id_from_selected_id;
-use notedeck::JobsCache;
use notedeck::{NoteAction, NoteContext};
use notedeck_ui::note::NoteResponse;
use notedeck_ui::{NoteOptions, NoteView};
@@ -16,7 +15,6 @@ pub struct ThreadView<'a, 'd> {
note_options: NoteOptions,
col: usize,
note_context: &'a mut NoteContext<'d>,
- jobs: &'a mut JobsCache,
}
impl<'a, 'd> ThreadView<'a, 'd> {
@@ -26,7 +24,6 @@ impl<'a, 'd> ThreadView<'a, 'd> {
selected_note_id: &'a [u8; 32],
note_options: NoteOptions,
note_context: &'a mut NoteContext<'d>,
- jobs: &'a mut JobsCache,
col: usize,
) -> Self {
ThreadView {
@@ -34,7 +31,6 @@ impl<'a, 'd> ThreadView<'a, 'd> {
selected_note_id,
note_options,
note_context,
- jobs,
col,
}
}
@@ -136,15 +132,7 @@ impl<'a, 'd> ThreadView<'a, 'd> {
ui.colored_label(ui.visuals().error_fg_color, "LOADING NOTES");
}
- show_notes(
- ui,
- list,
- ¬es,
- self.note_context,
- self.note_options,
- self.jobs,
- txn,
- )
+ show_notes(ui, list, ¬es, self.note_context, self.note_options, txn)
}
}
@@ -155,7 +143,6 @@ fn show_notes(
thread_notes: &ThreadNotes,
note_context: &mut NoteContext<'_>,
flags: NoteOptions,
- jobs: &mut JobsCache,
txn: &Transaction,
) -> Option<NoteAction> {
let mut action = None;
@@ -185,7 +172,7 @@ fn show_notes(
return 1;
}
- let resp = note.show(note_context, flags, jobs, ui);
+ let resp = note.show(note_context, flags, ui);
action = if cur_index == selected_note_index {
resp.action.and_then(strip_note_action)
@@ -326,11 +313,10 @@ impl<'a> ThreadNote<'a> {
&self,
note_context: &'a mut NoteContext<'_>,
flags: NoteOptions,
- jobs: &'a mut JobsCache,
ui: &mut egui::Ui,
) -> NoteResponse {
let inner = notedeck_ui::padding(8.0, ui, |ui| {
- NoteView::new(note_context, &self.note, self.options(flags), jobs)
+ NoteView::new(note_context, &self.note, self.options(flags))
.selected_style(self.note_type.is_selected())
.unread_indicator(self.unread_and_have_replies)
.show(ui)
diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs
@@ -6,7 +6,7 @@ use nostrdb::{Note, ProfileRecord, Transaction};
use notedeck::fonts::get_font_size;
use notedeck::name::get_display_name;
use notedeck::ui::is_narrow;
-use notedeck::{tr_plural, JobsCache, Muted, NotedeckTextStyle};
+use notedeck::{tr_plural, Muted, NotedeckTextStyle};
use notedeck_ui::app_images::{like_image_filled, repost_image};
use notedeck_ui::{ProfilePic, ProfilePreview};
use std::f32::consts::PI;
@@ -30,7 +30,6 @@ pub struct TimelineView<'a, 'd> {
timeline_cache: &'a mut TimelineCache,
note_options: NoteOptions,
note_context: &'a mut NoteContext<'d>,
- jobs: &'a mut JobsCache,
col: usize,
scroll_to_top: bool,
}
@@ -42,7 +41,6 @@ impl<'a, 'd> TimelineView<'a, 'd> {
timeline_cache: &'a mut TimelineCache,
note_context: &'a mut NoteContext<'d>,
note_options: NoteOptions,
- jobs: &'a mut JobsCache,
col: usize,
) -> Self {
let scroll_to_top = false;
@@ -51,7 +49,6 @@ impl<'a, 'd> TimelineView<'a, 'd> {
timeline_cache,
note_options,
note_context,
- jobs,
col,
scroll_to_top,
}
@@ -64,7 +61,6 @@ impl<'a, 'd> TimelineView<'a, 'd> {
self.timeline_cache,
self.note_options,
self.note_context,
- self.jobs,
self.col,
self.scroll_to_top,
)
@@ -93,7 +89,6 @@ fn timeline_ui(
timeline_cache: &mut TimelineCache,
mut note_options: NoteOptions,
note_context: &mut NoteContext,
- jobs: &mut JobsCache,
col: usize,
scroll_to_top: bool,
) -> BodyResponse<NoteAction> {
@@ -185,14 +180,7 @@ fn timeline_ui(
note_options.set(NoteOptions::Notification, true)
}
- TimelineTabView::new(
- timeline.current_view(),
- note_options,
- &txn,
- note_context,
- jobs,
- )
- .show(ui)
+ TimelineTabView::new(timeline.current_view(), note_options, &txn, note_context).show(ui)
});
let at_top_after_scroll = scroll_output.state.offset.y == 0.0;
@@ -378,7 +366,6 @@ pub struct TimelineTabView<'a, 'd> {
note_options: NoteOptions,
txn: &'a Transaction,
note_context: &'a mut NoteContext<'d>,
- jobs: &'a mut JobsCache,
}
impl<'a, 'd> TimelineTabView<'a, 'd> {
@@ -388,14 +375,12 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
note_options: NoteOptions,
txn: &'a Transaction,
note_context: &'a mut NoteContext<'d>,
- jobs: &'a mut JobsCache,
) -> Self {
Self {
tab,
note_options,
txn,
note_context,
- jobs,
}
}
@@ -473,19 +458,14 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
}
match entry {
- NoteUnit::Single(_) => render_note(
- ui,
- self.note_context,
- self.note_options,
- self.jobs,
- &underlying_note,
- ),
+ NoteUnit::Single(_) => {
+ render_note(ui, self.note_context, self.note_options, &underlying_note)
+ }
NoteUnit::Composite(composite) => match composite {
CompositeUnit::Reaction(reaction_unit) => render_reaction_cluster(
ui,
self.note_context,
self.note_options,
- self.jobs,
mute,
self.txn,
&underlying_note,
@@ -495,7 +475,6 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
ui,
self.note_context,
self.note_options,
- self.jobs,
mute,
self.txn,
&underlying_note,
@@ -676,12 +655,11 @@ fn render_note(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
note_options: NoteOptions,
- jobs: &mut JobsCache,
note: &Note,
) -> RenderEntryResponse {
let mut action = None;
notedeck_ui::padding(8.0, ui, |ui| {
- let resp = NoteView::new(note_context, note, note_options, jobs).show(ui);
+ let resp = NoteView::new(note_context, note, note_options).show(ui);
if let Some(note_action) = resp.action {
action = Some(note_action);
@@ -699,7 +677,6 @@ fn render_reaction_cluster(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
note_options: NoteOptions,
- jobs: &mut JobsCache,
mute: &std::sync::Arc<Muted>,
txn: &Transaction,
underlying_note: &Note,
@@ -729,7 +706,6 @@ fn render_reaction_cluster(
ui,
note_context,
note_options | NoteOptions::Notification,
- jobs,
underlying_note,
profiles_to_show,
CompositeType::Reaction,
@@ -742,7 +718,6 @@ fn render_composite_entry(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
mut note_options: NoteOptions,
- jobs: &mut JobsCache,
underlying_note: &nostrdb::Note<'_>,
profiles_to_show: Vec<ProfileEntry>,
composite_type: CompositeType,
@@ -793,6 +768,7 @@ fn render_composite_entry(
profiles_to_show,
&composite_type,
note_context.img_cache,
+ note_context.jobs,
note_options.contains(NoteOptions::Notification),
)
},
@@ -867,7 +843,7 @@ fn render_composite_entry(
ui.add_space(48.0);
};
- NoteView::new(note_context, underlying_note, note_options, jobs).show(ui)
+ NoteView::new(note_context, underlying_note, note_options).show(ui)
})
.inner;
@@ -886,6 +862,7 @@ fn render_profiles(
profiles_to_show: Vec<ProfileEntry>,
composite_type: &CompositeType,
img_cache: &mut notedeck::Images,
+ jobs: ¬edeck::MediaJobSender,
notification: bool,
) -> PfpsResponse {
let mut action = None;
@@ -932,7 +909,7 @@ fn render_profiles(
profiling::scope!("actual rendering individual pfp");
let mut widget =
- ProfilePic::from_profile_or_default(img_cache, entry.record.as_ref())
+ ProfilePic::from_profile_or_default(img_cache, jobs, entry.record.as_ref())
.size(24.0)
.sense(Sense::click());
let mut resp = ui.put(rect, &mut widget);
@@ -941,7 +918,7 @@ fn render_profiles(
if let Some(record) = entry.record.as_ref() {
resp = resp.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
- ui.add(ProfilePreview::new(record, img_cache));
+ ui.add(ProfilePreview::new(record, img_cache, jobs));
});
}
@@ -977,7 +954,6 @@ fn render_repost_cluster(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
note_options: NoteOptions,
- jobs: &mut JobsCache,
mute: &std::sync::Arc<Muted>,
txn: &Transaction,
underlying_note: &Note,
@@ -997,7 +973,6 @@ fn render_repost_cluster(
ui,
note_context,
note_options,
- jobs,
underlying_note,
profiles_to_show,
CompositeType::Repost,
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -8,7 +8,7 @@ use egui_wgpu::RenderState;
use enostr::KeypairUnowned;
use futures::StreamExt;
use nostrdb::Transaction;
-use notedeck::{AppAction, AppContext, AppResponse, JobsCache};
+use notedeck::{AppAction, AppContext, AppResponse};
use std::collections::HashMap;
use std::string::ToString;
use std::sync::mpsc::{self, Receiver};
@@ -43,7 +43,6 @@ pub struct Dave {
client: async_openai::Client<OpenAIConfig>,
incoming_tokens: Option<Receiver<DaveApiResponse>>,
model_config: ModelConfig,
- jobs: JobsCache,
}
/// Calculate an anonymous user_id from a keypair
@@ -108,7 +107,6 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
input,
model_config,
chat: vec![],
- jobs: JobsCache::default(),
}
}
@@ -189,11 +187,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
DaveResponse::default()
*/
- DaveUi::new(self.model_config.trial, &self.chat, &mut self.input).ui(
- app_ctx,
- &mut self.jobs,
- ui,
- )
+ DaveUi::new(self.model_config.trial, &self.chat, &mut self.input).ui(app_ctx, ui)
}
fn handle_new_chat(&mut self) {
diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs
@@ -5,7 +5,7 @@ use crate::{
use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers};
use nostrdb::{Ndb, Transaction};
use notedeck::{
- tr, Accounts, AppContext, Images, JobsCache, Localization, NoteAction, NoteContext,
+ tr, Accounts, AppContext, Images, Localization, MediaJobSender, NoteAction, NoteContext,
};
use notedeck_ui::{app_images, icons::search_icon, NoteOptions, ProfilePic};
@@ -86,12 +86,7 @@ impl<'a> DaveUi<'a> {
}
/// The main render function. Call this to render Dave
- pub fn ui(
- &mut self,
- app_ctx: &mut AppContext,
- jobs: &mut JobsCache,
- ui: &mut egui::Ui,
- ) -> DaveResponse {
+ pub fn ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse {
let action = top_buttons_ui(app_ctx, ui);
egui::Frame::NONE
@@ -118,7 +113,7 @@ impl<'a> DaveUi<'a> {
.show(ui, |ui| {
Self::chat_frame(ui.ctx())
.show(ui, |ui| {
- ui.vertical(|ui| self.render_chat(app_ctx, jobs, ui)).inner
+ ui.vertical(|ui| self.render_chat(app_ctx, ui)).inner
})
.inner
})
@@ -152,12 +147,7 @@ impl<'a> DaveUi<'a> {
}
/// Render a chat message (user, assistant, tool call/response, etc)
- fn render_chat(
- &self,
- ctx: &mut AppContext,
- jobs: &mut JobsCache,
- ui: &mut egui::Ui,
- ) -> Option<NoteAction> {
+ fn render_chat(&self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<NoteAction> {
let mut action: Option<NoteAction> = None;
for message in self.chat {
let r = match message {
@@ -182,7 +172,7 @@ impl<'a> DaveUi<'a> {
// have a debug option to show this
None
}
- Message::ToolCalls(toolcalls) => Self::tool_calls_ui(ctx, jobs, toolcalls, ui),
+ Message::ToolCalls(toolcalls) => Self::tool_calls_ui(ctx, toolcalls, ui),
};
if r.is_some() {
@@ -201,13 +191,18 @@ impl<'a> DaveUi<'a> {
ui.add(search_icon(16.0, 16.0));
ui.add_space(8.0);
- query_call_ui(ctx.img_cache, ctx.ndb, query_call, ui);
+ query_call_ui(
+ ctx.img_cache,
+ ctx.ndb,
+ query_call,
+ ctx.media_jobs.sender(),
+ ui,
+ );
}
/// The ai has asked us to render some notes, so we do that here
fn present_notes_ui(
ctx: &mut AppContext,
- jobs: &mut JobsCache,
call: &PresentNotesCall,
ui: &mut egui::Ui,
) -> Option<NoteAction> {
@@ -218,7 +213,7 @@ impl<'a> DaveUi<'a> {
note_cache: ctx.note_cache,
zaps: ctx.zaps,
pool: ctx.pool,
- job_pool: ctx.job_pool,
+ jobs: ctx.media_jobs.sender(),
unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard,
i18n: ctx.i18n,
@@ -249,7 +244,6 @@ impl<'a> DaveUi<'a> {
&mut note_context,
¬e,
NoteOptions::default(),
- jobs,
)
.preview_style()
.hide_media(true)
@@ -272,7 +266,6 @@ impl<'a> DaveUi<'a> {
fn tool_calls_ui(
ctx: &mut AppContext,
- jobs: &mut JobsCache,
toolcalls: &[ToolCall],
ui: &mut egui::Ui,
) -> Option<NoteAction> {
@@ -282,7 +275,7 @@ impl<'a> DaveUi<'a> {
for call in toolcalls {
match call.calls() {
ToolCalls::PresentNotes(call) => {
- let r = Self::present_notes_ui(ctx, jobs, call, ui);
+ let r = Self::present_notes_ui(ctx, call, ui);
if r.is_some() {
note_action = r;
}
@@ -399,7 +392,13 @@ fn new_chat_button() -> impl egui::Widget {
}
}
-fn query_call_ui(cache: &mut notedeck::Images, ndb: &Ndb, query: &QueryCall, ui: &mut egui::Ui) {
+fn query_call_ui(
+ cache: &mut notedeck::Images,
+ ndb: &Ndb,
+ query: &QueryCall,
+ jobs: &MediaJobSender,
+ ui: &mut egui::Ui,
+) {
ui.spacing_mut().item_spacing.x = 8.0;
if let Some(pubkey) = query.author() {
let txn = Transaction::new(ndb).unwrap();
@@ -409,6 +408,7 @@ fn query_call_ui(cache: &mut notedeck::Images, ndb: &Ndb, query: &QueryCall, ui:
ui.add(
&mut ProfilePic::from_profile_or_default(
cache,
+ jobs,
ndb.get_profile_by_pubkey(&txn, pubkey.bytes())
.ok()
.as_ref(),
@@ -489,7 +489,13 @@ fn top_buttons_ui(app_ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<DaveAct
let r = ui
.put(
rect,
- &mut pfp_button(&txn, app_ctx.accounts, app_ctx.img_cache, app_ctx.ndb),
+ &mut pfp_button(
+ &txn,
+ app_ctx.accounts,
+ app_ctx.img_cache,
+ app_ctx.ndb,
+ app_ctx.media_jobs.sender(),
+ ),
)
.on_hover_cursor(egui::CursorIcon::PointingHand);
@@ -512,13 +518,14 @@ fn pfp_button<'me, 'a>(
accounts: &Accounts,
img_cache: &'me mut Images,
ndb: &Ndb,
+ jobs: &'me MediaJobSender,
) -> ProfilePic<'me, 'a> {
let account = accounts.get_selected_account();
let profile = ndb
.get_profile_by_pubkey(txn, account.key.pubkey.bytes())
.ok();
- ProfilePic::from_profile_or_default(img_cache, profile.as_ref())
+ ProfilePic::from_profile_or_default(img_cache, jobs, profile.as_ref())
.size(24.0)
.sense(egui::Sense::click())
}
diff --git a/crates/notedeck_ui/src/media/viewer.rs b/crates/notedeck_ui/src/media/viewer.rs
@@ -1,7 +1,7 @@
use bitflags::bitflags;
use egui::{emath::TSTransform, pos2, Color32, Rangef, Rect};
use notedeck::media::{AnimationMode, MediaInfo, ViewMediaInfo};
-use notedeck::{ImageType, Images};
+use notedeck::{ImageType, Images, MediaJobSender};
bitflags! {
#[repr(transparent)]
@@ -90,23 +90,33 @@ impl<'a> MediaViewer<'a> {
self
}
- pub fn ui(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
+ pub fn ui(
+ &mut self,
+ images: &mut Images,
+ jobs: &MediaJobSender,
+ ui: &mut egui::Ui,
+ ) -> egui::Response {
if self.state.flags.contains(MediaViewerFlags::Fullscreen) {
egui::Window::new("Media Viewer")
.title_bar(false)
.fixed_size(ui.ctx().screen_rect().size())
.fixed_pos(ui.ctx().screen_rect().min)
.frame(egui::Frame::NONE)
- .show(ui.ctx(), |ui| self.ui_content(images, ui))
+ .show(ui.ctx(), |ui| self.ui_content(images, jobs, ui))
.unwrap() // SAFETY: we are always open
.inner
.unwrap()
} else {
- self.ui_content(images, ui)
+ self.ui_content(images, jobs, ui)
}
}
- fn ui_content(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
+ fn ui_content(
+ &mut self,
+ images: &mut Images,
+ jobs: &MediaJobSender,
+ ui: &mut egui::Ui,
+ ) -> egui::Response {
let avail_rect = ui.available_rect_before_wrap();
let scene_rect = if let Some(scene_rect) = self.state.scene_rect {
@@ -132,7 +142,7 @@ impl<'a> MediaViewer<'a> {
let mut trans_rect = if transitioning {
let clicked_img = &self.state.media_info.clicked_media();
let src_pos = &clicked_img.original_position;
- let in_scene_pos = Self::first_image_rect(ui, clicked_img, images);
+ let in_scene_pos = Self::first_image_rect(ui, clicked_img, images, jobs);
transition_scene_rect(
&avail_rect,
&zoom_range,
@@ -161,7 +171,7 @@ impl<'a> MediaViewer<'a> {
*/
let resp = scene.show(ui, &mut trans_rect, |ui| {
- Self::render_image_tiles(&self.state.media_info.medias, images, ui, open_amount);
+ Self::render_image_tiles(&self.state.media_info.medias, images, jobs, ui, open_amount);
});
self.state.scene_rect = Some(trans_rect);
@@ -174,9 +184,15 @@ impl<'a> MediaViewer<'a> {
///
/// TODO(jb55): replace this with a "placed" variant once
/// we have image layouts
- fn first_image_rect(ui: &mut egui::Ui, media: &MediaInfo, images: &mut Images) -> Rect {
+ fn first_image_rect(
+ ui: &mut egui::Ui,
+ media: &MediaInfo,
+ images: &mut Images,
+ jobs: &MediaJobSender,
+ ) -> Rect {
// fetch image texture
let Some(texture) = images.latest_texture(
+ jobs,
ui,
&media.url,
ImageType::Content(None),
@@ -204,6 +220,7 @@ impl<'a> MediaViewer<'a> {
fn render_image_tiles(
infos: &[MediaInfo],
images: &mut Images,
+ jobs: &MediaJobSender,
ui: &mut egui::Ui,
open_amount: f32,
) {
@@ -214,6 +231,7 @@ impl<'a> MediaViewer<'a> {
// we want to continually redraw things in the gallery
let Some(texture) = images.latest_texture(
+ jobs,
ui,
url,
ImageType::Content(None),
diff --git a/crates/notedeck_ui/src/mention.rs b/crates/notedeck_ui/src/mention.rs
@@ -2,11 +2,12 @@ use crate::ProfilePreview;
use egui::Sense;
use enostr::Pubkey;
use nostrdb::{Ndb, Transaction};
-use notedeck::{name::get_display_name, Images, NoteAction, NotedeckTextStyle};
+use notedeck::{name::get_display_name, Images, MediaJobSender, NoteAction, NotedeckTextStyle};
pub struct Mention<'a> {
ndb: &'a Ndb,
img_cache: &'a mut Images,
+ jobs: &'a MediaJobSender,
txn: &'a Transaction,
pk: &'a [u8; 32],
selectable: bool,
@@ -17,6 +18,7 @@ impl<'a> Mention<'a> {
pub fn new(
ndb: &'a Ndb,
img_cache: &'a mut Images,
+ jobs: &'a MediaJobSender,
txn: &'a Transaction,
pk: &'a [u8; 32],
) -> Self {
@@ -29,6 +31,7 @@ impl<'a> Mention<'a> {
pk,
selectable,
size,
+ jobs,
}
}
@@ -46,6 +49,7 @@ impl<'a> Mention<'a> {
mention_ui(
self.ndb,
self.img_cache,
+ self.jobs,
self.txn,
self.pk,
ui,
@@ -60,6 +64,7 @@ impl<'a> Mention<'a> {
fn mention_ui(
ndb: &Ndb,
img_cache: &mut Images,
+ jobs: &MediaJobSender,
txn: &Transaction,
pk: &[u8; 32],
ui: &mut egui::Ui,
@@ -99,7 +104,7 @@ fn mention_ui(
if let Some(rec) = profile.as_ref() {
resp.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
- ui.add(ProfilePreview::new(rec, img_cache));
+ ui.add(ProfilePreview::new(rec, img_cache, jobs));
});
}
diff --git a/crates/notedeck_ui/src/nip51_set.rs b/crates/notedeck_ui/src/nip51_set.rs
@@ -4,8 +4,8 @@ use enostr::Pubkey;
use hashbrown::{hash_map::RawEntryMut, HashMap};
use nostrdb::{Ndb, ProfileRecord, Transaction};
use notedeck::{
- fonts::get_font_size, get_profile_url, name::get_display_name, tr, Images, JobPool, JobsCache,
- Localization, Nip51Set, Nip51SetCache, NotedeckTextStyle,
+ fonts::get_font_size, get_profile_url, name::get_display_name, tr, Images, Localization,
+ MediaJobSender, Nip51Set, Nip51SetCache, NotedeckTextStyle,
};
use crate::{
@@ -19,8 +19,7 @@ pub struct Nip51SetWidget<'a> {
ndb: &'a Ndb,
images: &'a mut Images,
loc: &'a mut Localization,
- job_pool: &'a mut JobPool,
- jobs: &'a mut JobsCache,
+ jobs: &'a MediaJobSender,
flags: Nip51SetWidgetFlags,
}
@@ -53,8 +52,7 @@ impl<'a> Nip51SetWidget<'a> {
ndb: &'a Ndb,
loc: &'a mut Localization,
images: &'a mut Images,
- job_pool: &'a mut JobPool,
- jobs: &'a mut JobsCache,
+ jobs: &'a MediaJobSender,
) -> Self {
Self {
state,
@@ -62,7 +60,6 @@ impl<'a> Nip51SetWidget<'a> {
ndb,
loc,
images,
- job_pool,
jobs,
flags: Nip51SetWidgetFlags::default(),
}
@@ -92,7 +89,6 @@ impl<'a> Nip51SetWidget<'a> {
self.ui_state,
self.ndb,
self.images,
- self.job_pool,
self.jobs,
self.loc,
self.flags.contains(Nip51SetWidgetFlags::TRUST_IMAGES),
@@ -157,8 +153,7 @@ fn render_pack(
ui_state: &mut Nip51SetUiCache,
ndb: &Ndb,
images: &mut Images,
- job_pool: &mut JobPool,
- jobs: &mut JobsCache,
+ jobs: &MediaJobSender,
loc: &mut Localization,
image_trusted: bool,
) -> Option<Nip51SetWidgetAction> {
@@ -175,7 +170,6 @@ fn render_pack(
let media_rect = render_media(
ui,
images,
- job_pool,
jobs,
&media,
image_trusted,
@@ -250,7 +244,7 @@ fn render_pack(
};
ui.separator();
- if render_profile_item(ui, images, m_profile.as_ref(), cur_state) {
+ if render_profile_item(ui, images, jobs, m_profile.as_ref(), cur_state) {
resp = Some(Nip51SetWidgetAction::ViewProfile(*pk));
}
}
@@ -263,6 +257,7 @@ const PFP_SIZE: f32 = 32.0;
fn render_profile_item(
ui: &mut egui::Ui,
images: &mut Images,
+ jobs: &MediaJobSender,
profile: Option<&ProfileRecord>,
checked: &mut bool,
) -> bool {
@@ -294,7 +289,7 @@ fn render_profile_item(
let _ = ui.allocate_new_ui(UiBuilder::new().max_rect(pfp_rect), |ui| {
let pfp_resp = ui.add(
- &mut ProfilePic::new(images, get_profile_url(profile))
+ &mut ProfilePic::new(images, jobs, get_profile_url(profile))
.sense(Sense::click())
.size(PFP_SIZE),
);
diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs
@@ -6,8 +6,8 @@ use crate::{
use egui::{Color32, Hyperlink, Label, RichText};
use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
use notedeck::Localization;
+use notedeck::RenderableMedia;
use notedeck::{time_format, update_imeta_blurhashes, NoteCache, NoteContext, NotedeckTextStyle};
-use notedeck::{JobsCache, RenderableMedia};
use tracing::warn;
pub struct NoteContents<'a, 'd> {
@@ -16,7 +16,6 @@ pub struct NoteContents<'a, 'd> {
note: &'a Note<'a>,
options: NoteOptions,
pub action: Option<NoteAction>,
- jobs: &'a mut JobsCache,
}
impl<'a, 'd> NoteContents<'a, 'd> {
@@ -26,7 +25,6 @@ impl<'a, 'd> NoteContents<'a, 'd> {
txn: &'a Transaction,
note: &'a Note,
options: NoteOptions,
- jobs: &'a mut JobsCache,
) -> Self {
NoteContents {
note_context,
@@ -34,21 +32,13 @@ impl<'a, 'd> NoteContents<'a, 'd> {
note,
options,
action: None,
- jobs,
}
}
}
impl egui::Widget for &mut NoteContents<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
- let result = render_note_contents(
- ui,
- self.note_context,
- self.txn,
- self.note,
- self.options,
- self.jobs,
- );
+ let result = render_note_contents(ui, self.note_context, self.txn, self.note, self.options);
self.action = result.action;
result.response
}
@@ -83,7 +73,6 @@ pub fn render_note_preview(
id: &[u8; 32],
parent: NoteKey,
note_options: NoteOptions,
- jobs: &mut JobsCache,
) -> NoteResponse {
let note = if let Ok(note) = note_context.ndb.get_note_by_id(txn, id) {
// TODO: support other preview kinds
@@ -112,7 +101,7 @@ pub fn render_note_preview(
*/
};
- NoteView::new(note_context, ¬e, note_options, jobs)
+ NoteView::new(note_context, ¬e, note_options)
.preview_style()
.parent(parent)
.show(ui)
@@ -125,9 +114,8 @@ fn render_note_contents(
txn: &Transaction,
note: &Note,
options: NoteOptions,
- jobs: &mut JobsCache,
) -> NoteResponse {
- let response = render_undecorated_note_contents(ui, note_context, txn, note, options, jobs);
+ let response = render_undecorated_note_contents(ui, note_context, txn, note, options);
ui.horizontal_wrapped(|ui| {
note_bottom_metadata_ui(
@@ -169,7 +157,6 @@ fn render_undecorated_note_contents<'a>(
txn: &Transaction,
note: &'a Note,
options: NoteOptions,
- jobs: &mut JobsCache,
) -> NoteResponse {
let note_key = note.key().expect("todo: implement non-db notes");
let selectable = options.contains(NoteOptions::SelectableText);
@@ -208,6 +195,7 @@ fn render_undecorated_note_contents<'a>(
let act = crate::Mention::new(
note_context.ndb,
note_context.img_cache,
+ note_context.jobs,
txn,
profile.pubkey(),
)
@@ -223,6 +211,7 @@ fn render_undecorated_note_contents<'a>(
let act = crate::Mention::new(
note_context.ndb,
note_context.img_cache,
+ note_context.jobs,
txn,
npub.pubkey(),
)
@@ -355,7 +344,7 @@ fn render_undecorated_note_contents<'a>(
});
let preview_note_action = inline_note.and_then(|(id, _)| {
- render_note_preview(ui, note_context, txn, id, note_key, options, jobs)
+ render_note_preview(ui, note_context, txn, id, note_key, options)
.action
.map(|a| match a {
NoteAction::Note { note_id, .. } => NoteAction::Note {
@@ -375,8 +364,7 @@ fn render_undecorated_note_contents<'a>(
media_action = image_carousel(
ui,
note_context.img_cache,
- note_context.job_pool,
- jobs,
+ note_context.jobs,
&supported_medias,
carousel_id,
note_context.i18n,
diff --git a/crates/notedeck_ui/src/note/media.rs b/crates/notedeck_ui/src/note/media.rs
@@ -1,21 +1,18 @@
-use std::path::Path;
-
use bitflags::bitflags;
use egui::{
vec2, Button, Color32, Context, CornerRadius, FontId, Image, InnerResponse, Response,
TextureHandle, Vec2,
};
+use notedeck::media::latest::ObfuscatedTexture;
+use notedeck::MediaJobSender;
use notedeck::{
- compute_blurhash, fonts::get_font_size, show_one_error_message, tr, BlurhashParams,
- GifStateMap, Images, Job, JobId, JobParams, JobPool, JobState, JobsCache, Localization,
- MediaAction, MediaCacheType, NotedeckTextStyle, ObfuscationType, PointDimensions,
- RenderableMedia, TexturedImage, TexturesCache,
+ fonts::get_font_size, show_one_error_message, tr, Images, Localization, MediaAction,
+ MediaCacheType, NotedeckTextStyle, RenderableMedia,
};
use crate::NoteOptions;
-use notedeck::media::gif::ensure_latest_texture;
-use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
-use notedeck::media::AnimationMode;
+use notedeck::media::images::ImageType;
+use notedeck::media::{AnimationMode, MediaRenderState};
use notedeck::media::{MediaInfo, ViewMediaInfo};
use crate::{app_images, AnimationHelper, PulseAlpha};
@@ -30,8 +27,7 @@ pub enum MediaViewAction {
pub fn image_carousel(
ui: &mut egui::Ui,
img_cache: &mut Images,
- job_pool: &mut JobPool,
- jobs: &mut JobsCache,
+ jobs: &MediaJobSender,
medias: &[RenderableMedia],
carousel_id: egui::Id,
i18n: &mut Localization,
@@ -65,10 +61,10 @@ pub fn image_carousel(
let media_response = render_media(
ui,
img_cache,
- job_pool,
jobs,
media,
- note_options.contains(NoteOptions::TrustMedia),
+ note_options.contains(NoteOptions::TrustMedia)
+ || img_cache.user_trusts_img(&media.url, media.media_type),
i18n,
size,
if note_options.contains(NoteOptions::NoAnimations) {
@@ -96,7 +92,6 @@ pub fn image_carousel(
if let Some((i, media_action)) = media_action {
action = media_action.into_media_action(
- ui.ctx(),
medias,
media_infos,
i,
@@ -119,8 +114,7 @@ pub fn image_carousel(
pub fn render_media(
ui: &mut egui::Ui,
img_cache: &mut Images,
- job_pool: &mut JobPool,
- jobs: &mut JobsCache,
+ jobs: &MediaJobSender,
media: &RenderableMedia,
trusted_media: bool,
i18n: &mut Localization,
@@ -134,23 +128,6 @@ pub fn render_media(
obfuscation_type: blur_type,
} = media;
- let cache = match media_type {
- MediaCacheType::Image => &mut img_cache.static_imgs,
- MediaCacheType::Gif => &mut img_cache.gifs,
- };
- let media_state = get_content_media_render_state(
- ui,
- job_pool,
- jobs,
- trusted_media,
- size,
- &mut cache.textures_cache,
- url,
- *media_type,
- &cache.cache_dir,
- blur_type,
- );
-
let animation_mode = animation_mode.unwrap_or_else(|| {
// if animations aren't disabled, we cap it at 24fps for gifs in carousels
let fps = match media_type {
@@ -159,17 +136,24 @@ pub fn render_media(
};
AnimationMode::Continuous { fps }
});
+ let media_state = if trusted_media {
+ img_cache.trusted_texture_loader().latest(
+ jobs,
+ ui,
+ url,
+ *media_type,
+ ImageType::Content(None),
+ animation_mode,
+ blur_type,
+ size,
+ )
+ } else {
+ img_cache
+ .untrusted_texture_loader()
+ .latest(jobs, ui, url, blur_type, size)
+ };
- render_media_internal(
- ui,
- &mut img_cache.gif_states,
- media_state,
- url,
- size,
- i18n,
- scale_flags,
- animation_mode,
- )
+ render_media_internal(ui, media_state, url, size, i18n, scale_flags)
}
pub enum MediaUIAction {
@@ -182,7 +166,6 @@ pub enum MediaUIAction {
impl MediaUIAction {
pub fn into_media_action(
self,
- ctx: &egui::Context,
medias: &[RenderableMedia],
responses: Vec<MediaInfo>,
selected: usize,
@@ -203,17 +186,9 @@ impl MediaUIAction {
let url = &medias[selected].url;
let cache = img_cache.get_cache(medias[selected].media_type);
let cache_type = cache.cache_type;
- let no_pfp_promise = notedeck::media::images::fetch_img(
- &cache.cache_dir,
- ctx,
- url,
- img_type,
- cache_type,
- );
Some(MediaAction::FetchImage {
url: url.to_owned(),
cache_type,
- no_pfp_promise,
})
}
@@ -227,7 +202,6 @@ impl MediaUIAction {
Some(MediaAction::FetchImage {
url: medias[selected].url.to_owned(),
cache_type,
- no_pfp_promise: fetch_no_pfp_promise(ctx, cache),
})
}
MediaUIAction::DoneLoading => Some(MediaAction::DoneLoading {
@@ -238,115 +212,6 @@ impl MediaUIAction {
}
}
-#[allow(clippy::too_many_arguments)]
-pub fn get_content_media_render_state<'a>(
- ui: &mut egui::Ui,
- job_pool: &'a mut JobPool,
- jobs: &'a mut JobsCache,
- media_trusted: bool,
- size: Vec2,
- cache: &'a mut TexturesCache,
- url: &'a str,
- cache_type: MediaCacheType,
- cache_dir: &Path,
- obfuscation_type: &'a ObfuscationType,
-) -> MediaRenderState<'a> {
- let render_type = if media_trusted {
- cache.handle_and_get_or_insert_loadable(url, || {
- notedeck::media::images::fetch_img(
- cache_dir,
- ui.ctx(),
- url,
- ImageType::Content(None),
- cache_type,
- )
- })
- } else if let Some(render_type) = cache.get_and_handle(url) {
- render_type
- } else {
- return MediaRenderState::Obfuscated(get_obfuscated(
- ui,
- url,
- obfuscation_type,
- job_pool,
- jobs,
- size,
- ));
- };
-
- match render_type {
- notedeck::LoadableTextureState::Pending => MediaRenderState::Shimmering(get_obfuscated(
- ui,
- url,
- obfuscation_type,
- job_pool,
- jobs,
- size,
- )),
- notedeck::LoadableTextureState::Error(e) => MediaRenderState::Error(e),
- notedeck::LoadableTextureState::Loading { actual_image_tex } => {
- let obfuscation = get_obfuscated(ui, url, obfuscation_type, job_pool, jobs, size);
- MediaRenderState::Transitioning {
- image: actual_image_tex,
- obfuscation,
- }
- }
- notedeck::LoadableTextureState::Loaded(textured_image) => {
- MediaRenderState::ActualImage(textured_image)
- }
- }
-}
-
-fn get_obfuscated<'a>(
- ui: &mut egui::Ui,
- url: &str,
- obfuscation_type: &'a ObfuscationType,
- job_pool: &'a mut JobPool,
- jobs: &'a mut JobsCache,
- size: Vec2,
-) -> ObfuscatedTexture<'a> {
- let ObfuscationType::Blurhash(renderable_blur) = obfuscation_type else {
- return ObfuscatedTexture::Default;
- };
-
- let params = BlurhashParams {
- blurhash: &renderable_blur.blurhash,
- url,
- ctx: ui.ctx(),
- };
-
- let available_points = PointDimensions {
- x: size.x,
- y: size.y,
- };
-
- let pixel_sizes = renderable_blur.scaled_pixel_dimensions(ui, available_points);
-
- let job_state = jobs.get_or_insert_with(
- job_pool,
- &JobId::Blurhash(url),
- Some(JobParams::Blurhash(params)),
- move |params| compute_blurhash(params, pixel_sizes),
- );
-
- let JobState::Completed(m_blur_job) = job_state else {
- return ObfuscatedTexture::Default;
- };
-
- #[allow(irrefutable_let_patterns)]
- let Job::Blurhash(m_texture_handle) = m_blur_job
- else {
- tracing::error!("Did not get the correct job type: {:?}", m_blur_job);
- return ObfuscatedTexture::Default;
- };
-
- let Some(texture_handle) = m_texture_handle else {
- return ObfuscatedTexture::Default;
- };
-
- ObfuscatedTexture::Blur(texture_handle)
-}
-
fn copy_link(i18n: &mut Localization, url: &str, img_resp: &Response) {
img_resp.context_menu(|ui| {
if ui
@@ -366,42 +231,27 @@ fn copy_link(i18n: &mut Localization, url: &str, img_resp: &Response) {
#[allow(clippy::too_many_arguments)]
fn render_media_internal(
ui: &mut egui::Ui,
- gifs: &mut GifStateMap,
render_state: MediaRenderState,
url: &str,
size: egui::Vec2,
i18n: &mut Localization,
scale_flags: ScaledTextureFlags,
- animation_mode: AnimationMode,
) -> egui::InnerResponse<Option<MediaUIAction>> {
match render_state {
MediaRenderState::ActualImage(image) => {
- let resp = render_success_media(
- ui,
- url,
- image,
- gifs,
- size,
- i18n,
- scale_flags,
- animation_mode,
- );
+ let resp = render_success_media(ui, url, image, size, i18n, scale_flags);
if resp.clicked() {
egui::InnerResponse::new(Some(MediaUIAction::Clicked), resp)
} else {
egui::InnerResponse::new(None, resp)
}
}
- MediaRenderState::Transitioning { image, obfuscation } => match obfuscation {
- ObfuscatedTexture::Blur(texture) => {
- let resp = render_blur_transition(
- ui,
- url,
- size,
- texture,
- image.get_first_texture(),
- scale_flags,
- );
+ MediaRenderState::Transitioning {
+ image: img_tex,
+ obfuscation,
+ } => match obfuscation {
+ ObfuscatedTexture::Blur(blur_tex) => {
+ let resp = render_blur_transition(ui, url, size, blur_tex, img_tex, scale_flags);
if resp.inner {
egui::InnerResponse::new(Some(MediaUIAction::DoneLoading), resp.response)
} else {
@@ -409,7 +259,7 @@ fn render_media_internal(
}
}
ObfuscatedTexture::Default => {
- let scaled = ScaledTexture::new(image.get_first_texture(), size, scale_flags);
+ let scaled = ScaledTexture::new(img_tex, size, scale_flags);
let resp = ui.add(scaled.get_image());
egui::InnerResponse::new(Some(MediaUIAction::DoneLoading), resp)
}
@@ -592,57 +442,16 @@ fn render_default_blur_bg(
response
}
-pub enum MediaRenderState<'a> {
- ActualImage(&'a mut TexturedImage),
- Transitioning {
- image: &'a mut TexturedImage,
- obfuscation: ObfuscatedTexture<'a>,
- },
- Error(&'a notedeck::Error),
- Shimmering(ObfuscatedTexture<'a>),
- Obfuscated(ObfuscatedTexture<'a>),
-}
-
-pub enum ObfuscatedTexture<'a> {
- Blur(&'a TextureHandle),
- Default,
-}
-
-/*
-pub(crate) fn find_renderable_media<'a>(
- urls: &mut UrlMimes,
- imeta: &'a HashMap<String, ImageMetadata>,
- url: &'a str,
-) -> Option<RenderableMedia> {
- let media_type = supported_mime_hosted_at_url(urls, url)?;
-
- let obfuscation_type = match imeta.get(url) {
- Some(blur) => ObfuscationType::Blurhash(blur.clone()),
- None => ObfuscationType::Default,
- };
-
- Some(RenderableMedia {
- url,
- media_type,
- obfuscation_type,
- })
-}
-*/
-
#[allow(clippy::too_many_arguments)]
fn render_success_media(
ui: &mut egui::Ui,
url: &str,
- tex: &mut TexturedImage,
- gifs: &mut GifStateMap,
+ tex: &TextureHandle,
size: Vec2,
i18n: &mut Localization,
scale_flags: ScaledTextureFlags,
- animation_mode: AnimationMode,
) -> Response {
- let texture = ensure_latest_texture(ui, url, gifs, tex, animation_mode);
-
- let scaled = ScaledTexture::new(&texture, size, scale_flags);
+ let scaled = ScaledTexture::new(tex, size, scale_flags);
let img_resp = ui.add(Button::image(scaled.get_image()).frame(false));
diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs
@@ -9,15 +9,14 @@ use crate::{widgets::x_button, ProfilePic, ProfilePreview, PulseAlpha, Username}
pub use contents::{render_note_preview, NoteContents};
pub use context::NoteContextButton;
-use notedeck::get_current_wallet;
use notedeck::note::{reaction_sent_id, ZapTargetAmount};
use notedeck::ui::is_narrow;
use notedeck::Accounts;
use notedeck::GlobalWallet;
use notedeck::Images;
-use notedeck::JobsCache;
use notedeck::Localization;
use notedeck::MediaAction;
+use notedeck::{get_current_wallet, MediaJobSender};
pub use options::NoteOptions;
pub use reply_description::reply_desc;
@@ -35,7 +34,6 @@ pub struct NoteView<'a, 'd> {
parent: Option<NoteKey>,
note: &'a nostrdb::Note<'a>,
flags: NoteOptions,
- jobs: &'a mut JobsCache,
}
pub struct NoteResponse {
@@ -83,7 +81,6 @@ impl<'a, 'd> NoteView<'a, 'd> {
note_context: &'a mut NoteContext<'d>,
note: &'a nostrdb::Note<'a>,
flags: NoteOptions,
- jobs: &'a mut JobsCache,
) -> Self {
let parent: Option<NoteKey> = None;
@@ -92,7 +89,6 @@ impl<'a, 'd> NoteView<'a, 'd> {
parent,
note,
flags,
- jobs,
}
}
@@ -256,7 +252,6 @@ impl<'a, 'd> NoteView<'a, 'd> {
txn,
self.note,
self.flags,
- self.jobs,
));
//});
})
@@ -291,13 +286,19 @@ impl<'a, 'd> NoteView<'a, 'd> {
Some(pic) => show_actual_pfp(
ui,
self.note_context.img_cache,
+ self.note_context.jobs,
pic,
pfp_size,
note_key,
profile,
),
- None => show_fallback_pfp(ui, self.note_context.img_cache, pfp_size),
+ None => show_fallback_pfp(
+ ui,
+ self.note_context.img_cache,
+ self.note_context.jobs,
+ pfp_size,
+ ),
}
}
@@ -423,22 +424,15 @@ impl<'a, 'd> NoteView<'a, 'd> {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
- note_action = reply_desc(
- ui,
- txn,
- ¬e_reply,
- self.note_context,
- self.flags,
- self.jobs,
- )
- .or(note_action.take());
+ note_action =
+ reply_desc(ui, txn, ¬e_reply, self.note_context, self.flags)
+ .or(note_action.take());
});
});
});
}
- let mut contents =
- NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs);
+ let mut contents = NoteContents::new(self.note_context, txn, self.note, self.flags);
ui.add(&mut contents);
@@ -535,20 +529,13 @@ impl<'a, 'd> NoteView<'a, 'd> {
return;
}
- note_action = reply_desc(
- ui,
- txn,
- ¬e_reply,
- self.note_context,
- self.flags,
- self.jobs,
- )
- .or(note_action.take());
+ note_action =
+ reply_desc(ui, txn, ¬e_reply, self.note_context, self.flags)
+ .or(note_action.take());
});
}
- let mut contents =
- NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs);
+ let mut contents = NoteContents::new(self.note_context, txn, self.note, self.flags);
ui.add(&mut contents);
note_action = contents.action.or(note_action);
@@ -713,6 +700,7 @@ impl PfpResponse {
fn show_actual_pfp(
ui: &mut egui::Ui,
images: &mut Images,
+ jobs: &MediaJobSender,
pic: &str,
pfp_size: i8,
note_key: NoteKey,
@@ -732,13 +720,13 @@ fn show_actual_pfp(
let resp = resp.on_hover_cursor(egui::CursorIcon::PointingHand);
- let mut pfp = ProfilePic::new(images, pic).size(size);
+ let mut pfp = ProfilePic::new(images, jobs, pic).size(size);
let pfp_resp = ui.put(rect, &mut pfp);
let action = pfp.action;
pfp_resp.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
- ui.add(ProfilePreview::new(profile.as_ref().unwrap(), images));
+ ui.add(ProfilePreview::new(profile.as_ref().unwrap(), images, jobs));
});
PfpResponse {
@@ -748,14 +736,20 @@ fn show_actual_pfp(
}
}
-fn show_fallback_pfp(ui: &mut egui::Ui, images: &mut Images, pfp_size: i8) -> PfpResponse {
+fn show_fallback_pfp(
+ ui: &mut egui::Ui,
+ images: &mut Images,
+ jobs: &MediaJobSender,
+ pfp_size: i8,
+) -> PfpResponse {
let sense = Sense::click();
// This has to match the expand size from the above case to
// prevent bounciness
let size = (pfp_size + NoteView::expand_size()) as f32;
let (rect, _response) = ui.allocate_exact_size(egui::vec2(size, size), sense);
- let mut pfp = ProfilePic::new(images, notedeck::profile::no_pfp_url()).size(pfp_size as f32);
+ let mut pfp =
+ ProfilePic::new(images, jobs, notedeck::profile::no_pfp_url()).size(pfp_size as f32);
let response = ui.put(rect, &mut pfp).interact(sense);
PfpResponse {
diff --git a/crates/notedeck_ui/src/note/reply_description.rs b/crates/notedeck_ui/src/note/reply_description.rs
@@ -3,7 +3,7 @@ use nostrdb::{NoteReply, Transaction};
use super::NoteOptions;
use crate::{note::NoteView, Mention};
-use notedeck::{tr, JobsCache, NoteAction, NoteContext};
+use notedeck::{tr, NoteAction, NoteContext};
// Rich text segment types for internationalized rendering
#[derive(Debug, Clone)]
@@ -106,7 +106,6 @@ fn render_text_segments(
txn: &Transaction,
note_context: &mut NoteContext,
note_options: NoteOptions,
- jobs: &mut JobsCache,
size: f32,
selectable: bool,
) -> Option<NoteAction> {
@@ -126,6 +125,7 @@ fn render_text_segments(
let action = Mention::new(
note_context.ndb,
note_context.img_cache,
+ note_context.jobs,
txn,
pubkey.expect("expected pubkey"),
)
@@ -163,7 +163,7 @@ fn render_text_segments(
if r.hovered() {
r.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(400.0);
- NoteView::new(note_context, ¬e, note_options, jobs)
+ NoteView::new(note_context, ¬e, note_options)
.actionbar(false)
.wide(true)
.show(ui);
@@ -197,7 +197,7 @@ fn render_text_segments(
if r.hovered() {
r.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(400.0);
- NoteView::new(note_context, ¬e, note_options, jobs)
+ NoteView::new(note_context, ¬e, note_options)
.actionbar(false)
.wide(true)
.show(ui);
@@ -219,7 +219,6 @@ pub fn reply_desc(
note_reply: &NoteReply,
note_context: &mut NoteContext,
note_options: NoteOptions,
- jobs: &mut JobsCache,
) -> Option<NoteAction> {
let size = 10.0;
let selectable = false;
@@ -242,7 +241,6 @@ pub fn reply_desc(
txn,
note_context,
note_options,
- jobs,
size,
selectable,
);
@@ -271,7 +269,6 @@ pub fn reply_desc(
txn,
note_context,
note_options,
- jobs,
size,
selectable,
)
@@ -294,7 +291,6 @@ pub fn reply_desc(
txn,
note_context,
note_options,
- jobs,
size,
selectable,
)
@@ -324,7 +320,6 @@ pub fn reply_desc(
txn,
note_context,
note_options,
- jobs,
size,
selectable,
)
@@ -345,7 +340,6 @@ pub fn reply_desc(
txn,
note_context,
note_options,
- jobs,
size,
selectable,
)
@@ -366,7 +360,6 @@ pub fn reply_desc(
txn,
note_context,
note_options,
- jobs,
size,
selectable,
)
diff --git a/crates/notedeck_ui/src/profile/picture.rs b/crates/notedeck_ui/src/profile/picture.rs
@@ -1,14 +1,15 @@
use egui::{vec2, InnerResponse, Sense, Stroke, TextureHandle};
-use notedeck::get_render_state;
-use notedeck::media::gif::ensure_latest_texture;
-use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
+use notedeck::media::images::ImageType;
+use notedeck::media::latest::LatestImageTex;
use notedeck::media::AnimationMode;
use notedeck::MediaAction;
+use notedeck::MediaJobSender;
use notedeck::{show_one_error_message, supported_mime_hosted_at_url, Images};
pub struct ProfilePic<'cache, 'url> {
cache: &'cache mut Images,
+ jobs: &'cache MediaJobSender,
url: &'url str,
size: f32,
sense: Sense,
@@ -22,6 +23,7 @@ impl egui::Widget for &mut ProfilePic<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let inner = render_pfp(
ui,
+ self.jobs,
self.cache,
self.url,
self.size,
@@ -37,12 +39,13 @@ impl egui::Widget for &mut ProfilePic<'_, '_> {
}
impl<'cache, 'url> ProfilePic<'cache, 'url> {
- pub fn new(cache: &'cache mut Images, url: &'url str) -> Self {
+ pub fn new(cache: &'cache mut Images, jobs: &'cache MediaJobSender, url: &'url str) -> Self {
let size = Self::default_size() as f32;
let sense = Sense::hover();
ProfilePic {
cache,
+ jobs,
sense,
url,
size,
@@ -68,17 +71,19 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
pub fn from_profile(
cache: &'cache mut Images,
+ jobs: &'cache MediaJobSender,
profile: &nostrdb::ProfileRecord<'url>,
) -> Option<Self> {
profile
.record()
.profile()
.and_then(|p| p.picture())
- .map(|url| ProfilePic::new(cache, url))
+ .map(|url| ProfilePic::new(cache, jobs, url))
}
pub fn from_profile_or_default(
cache: &'cache mut Images,
+ jobs: &'cache MediaJobSender,
profile: Option<&nostrdb::ProfileRecord<'url>>,
) -> Self {
let url = profile
@@ -87,7 +92,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
.and_then(|p| p.picture())
.unwrap_or(notedeck::profile::no_pfp_url());
- ProfilePic::new(cache, url)
+ ProfilePic::new(cache, jobs, url)
}
#[inline]
@@ -119,8 +124,10 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
}
#[profiling::function]
+#[allow(clippy::too_many_arguments)]
fn render_pfp(
ui: &mut egui::Ui,
+ jobs: &MediaJobSender,
img_cache: &mut Images,
url: &str,
ui_size: f32,
@@ -134,38 +141,29 @@ fn render_pfp(
let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url)
.unwrap_or(notedeck::MediaCacheType::Image);
- let cur_state = get_render_state(
+ let cur_state = img_cache.no_img_loading_tex_loader().latest_state(
+ jobs,
ui.ctx(),
- img_cache,
- cache_type,
url,
+ cache_type,
ImageType::Profile(img_size),
+ animation_mode,
);
- match cur_state.texture_state {
- notedeck::TextureState::Pending => {
+ match cur_state {
+ LatestImageTex::Pending => {
profiling::scope!("Render pending");
egui::InnerResponse::new(None, paint_circle(ui, ui_size, border, sense))
}
- notedeck::TextureState::Error(e) => {
+ LatestImageTex::Error(e) => {
profiling::scope!("Render error");
let r = paint_circle(ui, ui_size, border, sense);
show_one_error_message(ui, &format!("Failed to fetch profile at url {url}: {e}"));
- egui::InnerResponse::new(
- Some(MediaAction::FetchImage {
- url: url.to_owned(),
- cache_type,
- no_pfp_promise: fetch_no_pfp_promise(ui.ctx(), img_cache.get_cache(cache_type)),
- }),
- r,
- )
+ egui::InnerResponse::new(None, r)
}
- notedeck::TextureState::Loaded(textured_image) => {
+ LatestImageTex::Loaded(texture_handle) => {
profiling::scope!("Render loaded");
- let texture_handle =
- ensure_latest_texture(ui, url, cur_state.gifs, textured_image, animation_mode);
-
- egui::InnerResponse::new(None, pfp_image(ui, &texture_handle, ui_size, border, sense))
+ egui::InnerResponse::new(None, pfp_image(ui, texture_handle, ui_size, border, sense))
}
}
}
diff --git a/crates/notedeck_ui/src/profile/preview.rs b/crates/notedeck_ui/src/profile/preview.rs
@@ -4,7 +4,8 @@ use egui_extras::Size;
use nostrdb::ProfileRecord;
use notedeck::{
- name::get_display_name, profile::get_profile_url, tr, Images, Localization, NotedeckTextStyle,
+ name::get_display_name, profile::get_profile_url, tr, Images, Localization, MediaJobSender,
+ NotedeckTextStyle,
};
use super::{about_section_widget, banner, display_name_widget};
@@ -12,14 +13,20 @@ use super::{about_section_widget, banner, display_name_widget};
pub struct ProfilePreview<'a, 'cache> {
profile: &'a ProfileRecord<'a>,
cache: &'cache mut Images,
+ jobs: &'cache MediaJobSender,
banner_height: Size,
}
impl<'a, 'cache> ProfilePreview<'a, 'cache> {
- pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut Images) -> Self {
+ pub fn new(
+ profile: &'a ProfileRecord<'a>,
+ cache: &'cache mut Images,
+ jobs: &'cache MediaJobSender,
+ ) -> Self {
let banner_height = Size::exact(80.0);
ProfilePreview {
profile,
+ jobs,
cache,
banner_height,
}
@@ -40,7 +47,7 @@ impl<'a, 'cache> ProfilePreview<'a, 'cache> {
ui.put(
pfp_rect,
- &mut ProfilePic::new(self.cache, get_profile_url(Some(self.profile)))
+ &mut ProfilePic::new(self.cache, self.jobs, get_profile_url(Some(self.profile)))
.size(size)
.border(ProfilePic::border_stroke(ui)),
);
@@ -72,6 +79,7 @@ pub struct SimpleProfilePreview<'a, 'cache> {
profile: Option<&'a ProfileRecord<'a>>,
pub i18n: &'cache mut Localization,
cache: &'cache mut Images,
+ jobs: &'cache MediaJobSender,
is_nsec: bool,
}
@@ -79,6 +87,7 @@ impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
pub fn new(
profile: Option<&'a ProfileRecord<'a>>,
cache: &'cache mut Images,
+ jobs: &'cache MediaJobSender,
i18n: &'cache mut Localization,
is_nsec: bool,
) -> Self {
@@ -87,6 +96,7 @@ impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
cache,
is_nsec,
i18n,
+ jobs,
}
}
}
@@ -95,7 +105,10 @@ impl egui::Widget for SimpleProfilePreview<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
Frame::new()
.show(ui, |ui| {
- ui.add(&mut ProfilePic::new(self.cache, get_profile_url(self.profile)).size(48.0));
+ ui.add(
+ &mut ProfilePic::new(self.cache, self.jobs, get_profile_url(self.profile))
+ .size(48.0),
+ );
ui.vertical(|ui| {
ui.add(display_name_widget(&get_display_name(self.profile), true));
if !self.is_nsec {