Merge GIF support by kernel

kernelkind (20):
      use bincode
      update ehttp to 0.5.0
      introduce UrlMimes
      use mime_guess
      add SupportedMimeType
      rename ImageCache -> MediaCache
      Use TexturedImage in MediaCache
      render MediaCache method
      move MediaCache rendering to render_media_cache call
      support multiple media cache files
      introduce Images
      render Images method
      migrate to using Images instead of MediaCache directly
      URL mime hosted completeness
      handle UrlCache io
      introduce gif animation
      handle gif state
      integrate gifs
      use SupportedMimeType for media_upload
      render gif in PostView
This commit is contained in:
William Casarin
2025-02-26 12:17:34 -08:00
35 changed files with 1065 additions and 344 deletions

View File

@@ -2,7 +2,7 @@ use enostr::FullKeypair;
use nostrdb::Ndb;
use notedeck::{
Accounts, AccountsAction, AddAccountAction, ImageCache, SingleUnkIdAction, SwitchAccountAction,
Accounts, AccountsAction, AddAccountAction, Images, SingleUnkIdAction, SwitchAccountAction,
};
use crate::app::get_active_columns_mut;
@@ -27,7 +27,7 @@ pub fn render_accounts_route(
ui: &mut egui::Ui,
ndb: &Ndb,
col: usize,
img_cache: &mut ImageCache,
img_cache: &mut Images,
accounts: &mut Accounts,
decks: &mut DecksCache,
login_state: &mut AcquireKeyState,

View File

@@ -13,7 +13,7 @@ use crate::{
Result,
};
use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, ImageCache, UnknownIds};
use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, UnknownIds};
use enostr::{ClientMessage, Keypair, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
use uuid::Uuid;
@@ -172,6 +172,8 @@ fn unknown_id_send(unknown_ids: &mut UnknownIds, pool: &mut RelayPool) {
}
fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Context) {
app_ctx.img_cache.urls.cache.handle_io();
match damus.state {
DamusState::Initializing => {
damus.state = DamusState::Initialized;
@@ -464,7 +466,7 @@ impl Damus {
let decks_cache = DecksCache::default();
let path = DataPath::new(&data_path);
let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
let imgcache_dir = path.path(DataPathType::Cache);
let _ = std::fs::create_dir_all(imgcache_dir.clone());
let debug = true;

View File

@@ -0,0 +1,122 @@
use std::{
sync::mpsc::TryRecvError,
time::{Instant, SystemTime},
};
use egui::TextureHandle;
use notedeck::{GifState, GifStateMap, TexturedImage};
pub struct LatextTexture<'a> {
pub texture: &'a TextureHandle,
pub request_next_repaint: Option<SystemTime>,
}
/// This is necessary because other repaint calls can effectively steal our repaint request.
/// So we must keep on requesting to repaint at our desired time to ensure our repaint goes through.
/// See [`egui::Context::request_repaint_after`]
pub fn handle_repaint<'a>(ui: &egui::Ui, latest: LatextTexture<'a>) -> &'a TextureHandle {
if let Some(repaint) = latest.request_next_repaint {
if let Ok(dur) = repaint.duration_since(SystemTime::now()) {
ui.ctx().request_repaint_after(dur);
}
}
latest.texture
}
#[must_use = "caller should pass the return value to `gif::handle_repaint`"]
pub fn retrieve_latest_texture<'a>(
url: &str,
gifs: &'a mut GifStateMap,
cached_image: &'a mut TexturedImage,
) -> LatextTexture<'a> {
match cached_image {
TexturedImage::Static(texture) => LatextTexture {
texture,
request_next_repaint: None,
},
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;
}
}
}
}
let now = Instant::now();
let (texture, maybe_new_state, request_next_repaint) = match gifs.get(url) {
Some(prev_state) => {
let should_advance =
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 = SystemTime::now().checked_add(frame.delay);
(
&frame.texture,
Some(GifState {
last_frame_rendered: now,
last_frame_duration: frame.delay,
next_frame_time,
last_frame_index: maybe_new_index,
}),
next_frame_time,
)
}
None => {
let (tex, state) =
match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (&frame.texture, None),
None => (&animation.first_frame.texture, None),
};
(tex, state, prev_state.next_frame_time)
}
}
} else {
let (tex, state) = match animation.get_frame(prev_state.last_frame_index) {
Some(frame) => (&frame.texture, None),
None => (&animation.first_frame.texture, None),
};
(tex, state, prev_state.next_frame_time)
}
}
None => (
&animation.first_frame.texture,
Some(GifState {
last_frame_rendered: now,
last_frame_duration: animation.first_frame.delay,
next_frame_time: None,
last_frame_index: 0,
}),
None,
),
};
if let Some(new_state) = maybe_new_state {
gifs.insert(url.to_owned(), new_state);
}
LatextTexture {
texture,
request_next_repaint,
}
}
}
}

View File

@@ -1,16 +1,28 @@
use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint, TextureHandle};
use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint};
use image::codecs::gif::GifDecoder;
use image::imageops::FilterType;
use notedeck::ImageCache;
use image::AnimationDecoder;
use image::DynamicImage;
use image::FlatSamples;
use image::Frame;
use notedeck::Animation;
use notedeck::ImageFrame;
use notedeck::MediaCache;
use notedeck::MediaCacheType;
use notedeck::Result;
use notedeck::TextureFrame;
use notedeck::TexturedImage;
use poll_promise::Promise;
use std::collections::VecDeque;
use std::io::Cursor;
use std::path;
use std::path::PathBuf;
use std::sync::mpsc;
use std::sync::mpsc::SyncSender;
use std::thread;
use std::time::Duration;
use tokio::fs;
//pub type ImageCacheKey = String;
//pub type ImageCacheValue = Promise<Result<TextureHandle>>;
//pub type ImageCache = HashMap<String, ImageCacheValue>;
// NOTE(jb55): chatgpt wrote this because I was too dumb to
pub fn aspect_fill(
ui: &mut egui::Ui,
@@ -102,7 +114,7 @@ pub fn round_image(image: &mut ColorImage) {
}
}
fn process_pfp_bitmap(imgtyp: ImageType, image: &mut image::DynamicImage) -> ColorImage {
fn process_pfp_bitmap(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
#[cfg(feature = "profiling")]
puffin::profile_function!();
@@ -125,10 +137,10 @@ fn process_pfp_bitmap(imgtyp: ImageType, image: &mut image::DynamicImage) -> Col
if image.width() > smaller {
let excess = image.width() - smaller;
*image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
} else if image.height() > smaller {
let excess = image.height() - smaller;
*image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
}
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
@@ -166,8 +178,8 @@ fn parse_img_response(response: ehttp::Response, imgtyp: ImageType) -> Result<Co
} else if content_type.starts_with("image/") {
#[cfg(feature = "profiling")]
puffin::profile_scope!("load_from_memory");
let mut dyn_image = image::load_from_memory(&response.bytes)?;
Ok(process_pfp_bitmap(imgtyp, &mut dyn_image))
let dyn_image = image::load_from_memory(&response.bytes)?;
Ok(process_pfp_bitmap(imgtyp, dyn_image))
} else {
Err(format!("Expected image, found content-type {:?}", content_type).into())
}
@@ -177,28 +189,162 @@ fn fetch_img_from_disk(
ctx: &egui::Context,
url: &str,
path: &path::Path,
) -> Promise<Result<TextureHandle>> {
cache_type: MediaCacheType,
) -> Promise<Result<TexturedImage>> {
let ctx = ctx.clone();
let url = url.to_owned();
let path = path.to_owned();
Promise::spawn_async(async move {
let data = fs::read(path).await?;
let image_buffer = image::load_from_memory(&data).map_err(notedeck::Error::Image)?;
match cache_type {
MediaCacheType::Image => {
let data = fs::read(path).await?;
let image_buffer =
image::load_from_memory(&data).map_err(notedeck::Error::Image)?;
// TODO: remove unwrap here
let flat_samples = image_buffer.as_flat_samples_u8().unwrap();
let img = ColorImage::from_rgba_unmultiplied(
[
image_buffer.width() as usize,
image_buffer.height() as usize,
],
flat_samples.as_slice(),
);
Ok(ctx.load_texture(&url, img, Default::default()))
let img = buffer_to_color_image(
image_buffer.as_flat_samples_u8(),
image_buffer.width(),
image_buffer.height(),
);
Ok(TexturedImage::Static(ctx.load_texture(
&url,
img,
Default::default(),
)))
}
MediaCacheType::Gif => {
let gif_bytes = fs::read(path.clone()).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> {
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| notedeck::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::error!("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(notedeck::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: ctx.load_texture(format!("{}{}", url, index), color_img, Default::default()),
}
}
fn buffer_to_color_image(
samples: Option<FlatSamples<&[u8]>>,
width: u32,
height: u32,
) -> ColorImage {
// TODO(jb55): remove unwrap here
let flat_samples = samples.unwrap();
ColorImage::from_rgba_unmultiplied([width as usize, height as usize], flat_samples.as_slice())
}
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>> {
std::fs::read(path).map_err(|e| notedeck::Error::Generic(e.to_string()))
}
@@ -213,18 +359,19 @@ pub enum ImageType {
}
pub fn fetch_img(
img_cache: &ImageCache,
img_cache: &MediaCache,
ctx: &egui::Context,
url: &str,
imgtyp: ImageType,
) -> Promise<Result<TextureHandle>> {
let key = ImageCache::key(url);
cache_type: MediaCacheType,
) -> Promise<Result<TexturedImage>> {
let key = MediaCache::key(url);
let path = img_cache.cache_dir.join(key);
if path.exists() {
fetch_img_from_disk(ctx, url, &path)
fetch_img_from_disk(ctx, url, &path, cache_type)
} else {
fetch_img_from_net(&img_cache.cache_dir, ctx, url, imgtyp)
fetch_img_from_net(&img_cache.cache_dir, ctx, url, imgtyp, cache_type)
}
// TODO: fetch image from local cache
@@ -235,24 +382,43 @@ fn fetch_img_from_net(
ctx: &egui::Context,
url: &str,
imgtyp: ImageType,
) -> Promise<Result<TextureHandle>> {
cache_type: MediaCacheType,
) -> Promise<Result<TexturedImage>> {
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(notedeck::Error::Generic)
.and_then(|resp| parse_img_response(resp, imgtyp))
.map(|img| {
let texture_handle = ctx.load_texture(&cloned_url, img.clone(), Default::default());
let handle = response.map_err(notedeck::Error::Generic).and_then(|resp| {
match cache_type {
MediaCacheType::Image => {
let img = parse_img_response(resp, imgtyp);
img.map(|img| {
let texture_handle =
ctx.load_texture(&cloned_url, img.clone(), Default::default());
// write to disk
std::thread::spawn(move || ImageCache::write(&cache_path, &cloned_url, img));
// write to disk
std::thread::spawn(move || {
MediaCache::write(&cache_path, &cloned_url, img)
});
texture_handle
});
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_pfp_bitmap(imgtyp, img),
)
}
}
});
sender.send(handle); // send the results back to the UI thread.
ctx.request_repaint();

View File

@@ -15,6 +15,7 @@ mod deck_state;
mod decks;
mod draft;
mod frame_history;
mod gif;
mod images;
mod key_parsing;
pub mod login_manager;

View File

@@ -1,8 +1,9 @@
use std::{collections::BTreeMap, path::PathBuf};
use std::path::PathBuf;
use base64::{prelude::BASE64_URL_SAFE, Engine};
use ehttp::Request;
use nostrdb::{Note, NoteBuilder};
use notedeck::SupportedMimeType;
use poll_promise::Promise;
use sha2::{Digest, Sha256};
use url::Url;
@@ -104,15 +105,13 @@ fn create_nip96_request(
body.extend(file_contents);
body.extend(format!("\r\n--{}--\r\n", boundary).as_bytes());
let headers = {
let mut map = BTreeMap::new();
map.insert(
"Content-Type".to_owned(),
format!("multipart/form-data; boundary={boundary}"),
);
map.insert("Authorization".to_owned(), format!("Nostr {nip98_base64}"));
map
};
let headers = ehttp::Headers::new(&[
(
"Content-Type",
format!("multipart/form-data; boundary={boundary}").as_str(),
),
("Authorization", format!("Nostr {nip98_base64}").as_str()),
]);
Request {
method: "POST".to_string(),
@@ -234,13 +233,13 @@ fn find_nip94_ev_in_json(json: String) -> Result<Nip94Event, Error> {
pub struct MediaPath {
full_path: PathBuf,
file_name: String,
media_type: SupportedMediaType,
media_type: SupportedMimeType,
}
impl MediaPath {
pub fn new(path: PathBuf) -> Result<Self, Error> {
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
let media_type = SupportedMediaType::from_extension(ex)?;
let media_type = SupportedMimeType::from_extension(ex)?;
let file_name = path
.file_name()
.and_then(|name| name.to_str())
@@ -261,47 +260,6 @@ impl MediaPath {
}
}
#[derive(Debug)]
pub enum SupportedMediaType {
Png,
Jpeg,
Webp,
}
impl SupportedMediaType {
pub fn mime_extension(&self) -> &str {
match &self {
SupportedMediaType::Png => "png",
SupportedMediaType::Jpeg => "jpeg",
SupportedMediaType::Webp => "webp",
}
}
pub fn to_mime(&self) -> String {
format!("{}/{}", self.mime_type(), self.mime_extension())
}
fn mime_type(&self) -> String {
match &self {
SupportedMediaType::Png | SupportedMediaType::Jpeg | SupportedMediaType::Webp => {
"image"
}
}
.to_string()
}
fn from_extension(ext: &str) -> Result<Self, Error> {
match ext.to_lowercase().as_str() {
"jpeg" | "jpg" => Ok(SupportedMediaType::Jpeg),
"png" => Ok(SupportedMediaType::Png),
"webp" => Ok(SupportedMediaType::Webp),
unsupported_type => Err(Error::Generic(format!(
"{unsupported_type} is not a valid file type to upload."
))),
}
}
}
#[derive(Clone, Debug, serde::Deserialize)]
pub struct Nip94Event {
pub url: String,

View File

@@ -7,12 +7,12 @@ use crate::{
use enostr::Pubkey;
use nostrdb::Ndb;
use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, UnknownIds};
use notedeck::{Accounts, Images, MuteFun, NoteCache, UnknownIds};
#[allow(clippy::too_many_arguments)]
pub fn render_timeline_route(
ndb: &Ndb,
img_cache: &mut ImageCache,
img_cache: &mut Images,
unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache,
timeline_cache: &mut TimelineCache,
@@ -102,7 +102,7 @@ pub fn render_profile_route(
accounts: &Accounts,
ndb: &Ndb,
timeline_cache: &mut TimelineCache,
img_cache: &mut ImageCache,
img_cache: &mut Images,
note_cache: &mut NoteCache,
unknown_ids: &mut UnknownIds,
col: usize,

View File

@@ -3,14 +3,14 @@ use egui::{
Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, UiBuilder, Vec2,
};
use nostrdb::{Ndb, Transaction};
use notedeck::{Accounts, ImageCache};
use notedeck::{Accounts, Images};
use super::profile::preview::SimpleProfilePreview;
pub struct AccountsView<'a> {
ndb: &'a Ndb,
accounts: &'a Accounts,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
}
#[derive(Clone, Debug)]
@@ -27,7 +27,7 @@ enum ProfilePreviewAction {
}
impl<'a> AccountsView<'a> {
pub fn new(ndb: &'a Ndb, accounts: &'a Accounts, img_cache: &'a mut ImageCache) -> Self {
pub fn new(ndb: &'a Ndb, accounts: &'a Accounts, img_cache: &'a mut Images) -> Self {
AccountsView {
ndb,
accounts,
@@ -54,7 +54,7 @@ impl<'a> AccountsView<'a> {
ui: &mut Ui,
accounts: &Accounts,
ndb: &Ndb,
img_cache: &mut ImageCache,
img_cache: &mut Images,
) -> Option<AccountsViewResponse> {
let mut return_op: Option<AccountsViewResponse> = None;
ui.allocate_ui_with_layout(

View File

@@ -17,7 +17,7 @@ use crate::{
Damus,
};
use notedeck::{AppContext, ImageCache, NotedeckTextStyle, UserAccount};
use notedeck::{AppContext, Images, NotedeckTextStyle, UserAccount};
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
use super::{anim::AnimationHelper, padding, ProfilePreview};
@@ -163,7 +163,7 @@ impl AddColumnOption {
pub struct AddColumnView<'a> {
key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
cur_account: Option<&'a UserAccount>,
}
@@ -171,7 +171,7 @@ impl<'a> AddColumnView<'a> {
pub fn new(
key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
cur_account: Option<&'a UserAccount>,
) -> Self {
Self {

View File

@@ -16,11 +16,11 @@ use egui::Margin;
use egui::{RichText, Stroke, UiBuilder};
use enostr::Pubkey;
use nostrdb::{Ndb, Transaction};
use notedeck::{ImageCache, NotedeckTextStyle};
use notedeck::{Images, NotedeckTextStyle};
pub struct NavTitle<'a> {
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
columns: &'a Columns,
routes: &'a [Route],
col_id: usize,
@@ -29,7 +29,7 @@ pub struct NavTitle<'a> {
impl<'a> NavTitle<'a> {
pub fn new(
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
columns: &'a Columns,
routes: &'a [Route],
col_id: usize,

View File

@@ -0,0 +1,75 @@
use notedeck::{GifStateMap, Images, MediaCache, MediaCacheType, TexturedImage};
use crate::images::ImageType;
use super::ProfilePic;
#[allow(clippy::too_many_arguments)]
pub fn render_images(
ui: &mut egui::Ui,
images: &mut Images,
url: &str,
img_type: ImageType,
cache_type: MediaCacheType,
show_waiting: impl FnOnce(&mut egui::Ui),
show_error: impl FnOnce(&mut egui::Ui, String),
show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap),
) -> egui::Response {
let cache = match cache_type {
MediaCacheType::Image => &mut images.static_imgs,
MediaCacheType::Gif => &mut images.gifs,
};
render_media_cache(
ui,
cache,
&mut images.gif_states,
url,
img_type,
cache_type,
show_waiting,
show_error,
show_success,
)
}
#[allow(clippy::too_many_arguments)]
fn render_media_cache(
ui: &mut egui::Ui,
cache: &mut MediaCache,
gif_states: &mut GifStateMap,
url: &str,
img_type: ImageType,
cache_type: MediaCacheType,
show_waiting: impl FnOnce(&mut egui::Ui),
show_error: impl FnOnce(&mut egui::Ui, String),
show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap),
) -> egui::Response {
let m_cached_promise = cache.map().get(url);
if m_cached_promise.is_none() {
let res = crate::images::fetch_img(cache, ui.ctx(), url, img_type, cache_type.clone());
cache.map_mut().insert(url.to_owned(), res);
}
egui::Frame::none()
.show(ui, |ui| {
match cache.map_mut().get_mut(url).and_then(|p| p.ready_mut()) {
None => show_waiting(ui),
Some(Err(err)) => {
let err = err.to_string();
let no_pfp = crate::images::fetch_img(
cache,
ui.ctx(),
ProfilePic::no_pfp_url(),
ImageType::Profile(128),
cache_type,
);
cache.map_mut().insert(url.to_owned(), no_pfp);
show_error(ui, err)
}
Some(Ok(renderable_media)) => show_success(ui, url, renderable_media, gif_states),
}
})
.response
}

View File

@@ -3,11 +3,11 @@ use crate::{actionbar::NoteAction, profile::get_display_name, timeline::Timeline
use egui::Sense;
use enostr::Pubkey;
use nostrdb::{Ndb, Transaction};
use notedeck::ImageCache;
use notedeck::Images;
pub struct Mention<'a> {
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
txn: &'a Transaction,
pk: &'a [u8; 32],
selectable: bool,
@@ -17,7 +17,7 @@ pub struct Mention<'a> {
impl<'a> Mention<'a> {
pub fn new(
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
txn: &'a Transaction,
pk: &'a [u8; 32],
) -> Self {
@@ -62,9 +62,10 @@ impl egui::Widget for Mention<'_> {
}
}
#[allow(clippy::too_many_arguments)]
fn mention_ui(
ndb: &Ndb,
img_cache: &mut ImageCache,
img_cache: &mut Images,
txn: &Transaction,
pk: &[u8; 32],
ui: &mut egui::Ui,

View File

@@ -5,6 +5,7 @@ pub mod anim;
pub mod column;
pub mod configure_deck;
pub mod edit_deck;
pub mod images;
pub mod mention;
pub mod note;
pub mod preview;

View File

@@ -1,18 +1,19 @@
use crate::gif::{handle_repaint, retrieve_latest_texture};
use crate::ui::images::render_images;
use crate::ui::{
self,
note::{NoteOptions, NoteResponse},
ProfilePic,
};
use crate::{actionbar::NoteAction, images::ImageType, timeline::TimelineKind};
use egui::{Color32, Hyperlink, Image, RichText};
use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
use tracing::warn;
use notedeck::{ImageCache, NoteCache};
use notedeck::{supported_mime_hosted_at_url, Images, MediaCacheType, NoteCache};
pub struct NoteContents<'a> {
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
note_cache: &'a mut NoteCache,
txn: &'a Transaction,
note: &'a Note<'a>,
@@ -22,9 +23,10 @@ pub struct NoteContents<'a> {
}
impl<'a> NoteContents<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
note_cache: &'a mut NoteCache,
txn: &'a Transaction,
note: &'a Note,
@@ -72,7 +74,7 @@ pub fn render_note_preview(
ui: &mut egui::Ui,
ndb: &Ndb,
note_cache: &mut NoteCache,
img_cache: &mut ImageCache,
img_cache: &mut Images,
txn: &Transaction,
id: &[u8; 32],
parent: NoteKey,
@@ -126,15 +128,11 @@ pub fn render_note_preview(
.inner
}
fn is_image_link(url: &str) -> bool {
url.ends_with("png") || url.ends_with("jpg") || url.ends_with("jpeg")
}
#[allow(clippy::too_many_arguments)]
fn render_note_contents(
ui: &mut egui::Ui,
ndb: &Ndb,
img_cache: &mut ImageCache,
img_cache: &mut Images,
note_cache: &mut NoteCache,
txn: &Transaction,
note: &Note,
@@ -145,7 +143,7 @@ fn render_note_contents(
puffin::profile_function!();
let selectable = options.has_selectable_text();
let mut images: Vec<String> = vec![];
let mut images: Vec<(String, MediaCacheType)> = vec![];
let mut note_action: Option<NoteAction> = None;
let mut inline_note: Option<(&[u8; 32], &str)> = None;
let hide_media = options.has_hide_media();
@@ -211,9 +209,14 @@ fn render_note_contents(
}
BlockType::Url => {
let lower_url = block.as_str().to_lowercase();
if !hide_media && is_image_link(&lower_url) {
images.push(block.as_str().to_string());
if !hide_media {
let url = block.as_str().to_string();
if let Some(cache_type) =
supported_mime_hosted_at_url(&mut img_cache.urls, &url)
{
images.push((url, cache_type));
}
} else {
#[cfg(feature = "profiling")]
puffin::profile_scope!("url contents");
@@ -279,8 +282,8 @@ fn rot13(input: &str) -> String {
fn image_carousel(
ui: &mut egui::Ui,
img_cache: &mut ImageCache,
images: Vec<String>,
img_cache: &mut Images,
images: Vec<(String, MediaCacheType)>,
carousel_id: egui::Id,
) {
// let's make sure everything is within our area
@@ -294,56 +297,39 @@ fn image_carousel(
.id_salt(carousel_id)
.show(ui, |ui| {
ui.horizontal(|ui| {
for image in images {
// If the cache is empty, initiate the fetch
let m_cached_promise = img_cache.map().get(&image);
if m_cached_promise.is_none() {
let res = crate::images::fetch_img(
img_cache,
ui.ctx(),
&image,
ImageType::Content(width.round() as u32, height.round() as u32),
);
img_cache.map_mut().insert(image.to_owned(), res);
}
// What is the state of the fetch?
match img_cache.map()[&image].ready() {
// Still waiting
None => {
for (image, cache_type) in images {
render_images(
ui,
img_cache,
&image,
ImageType::Content(width.round() as u32, height.round() as u32),
cache_type,
|ui| {
ui.allocate_space(egui::vec2(spinsz, spinsz));
//ui.add(egui::Spinner::new().size(spinsz));
}
// Failed to fetch image!
Some(Err(_err)) => {
// FIXME - use content-specific error instead
let no_pfp = crate::images::fetch_img(
img_cache,
ui.ctx(),
ProfilePic::no_pfp_url(),
ImageType::Profile(128),
},
|ui, _| {
ui.allocate_space(egui::vec2(spinsz, spinsz));
},
|ui, url, renderable_media, gifs| {
let texture = handle_repaint(
ui,
retrieve_latest_texture(&image, gifs, renderable_media),
);
img_cache.map_mut().insert(image.to_owned(), no_pfp);
// spin until next pass
ui.allocate_space(egui::vec2(spinsz, spinsz));
//ui.add(egui::Spinner::new().size(spinsz));
}
// Use the previously resolved image
Some(Ok(img)) => {
let img_resp = ui.add(
Image::new(img)
Image::new(texture)
.max_height(height)
.rounding(5.0)
.fit_to_original_size(1.0),
);
img_resp.context_menu(|ui| {
if ui.button("Copy Link").clicked() {
ui.ctx().copy_text(image);
ui.ctx().copy_text(url.to_owned());
ui.close_menu();
}
});
}
}
},
);
}
})
.response

View File

@@ -25,14 +25,14 @@ use egui::emath::{pos2, Vec2};
use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense};
use enostr::{NoteId, Pubkey};
use nostrdb::{Ndb, Note, NoteKey, Transaction};
use notedeck::{CachedNote, ImageCache, NoteCache, NotedeckTextStyle};
use notedeck::{CachedNote, Images, NoteCache, NotedeckTextStyle};
use super::profile::preview::one_line_display_name_widget;
pub struct NoteView<'a> {
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
parent: Option<NoteKey>,
note: &'a nostrdb::Note<'a>,
flags: NoteOptions,
@@ -74,7 +74,7 @@ impl<'a> NoteView<'a> {
pub fn new(
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
note: &'a nostrdb::Note<'a>,
mut flags: NoteOptions,
) -> Self {

View File

@@ -1,8 +1,9 @@
use crate::draft::{Draft, Drafts, MentionHint};
use crate::images::fetch_img;
use crate::gif::{handle_repaint, retrieve_latest_texture};
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
use crate::post::{downcast_post_buffer, MentionType, NewPost};
use crate::profile::get_display_name;
use crate::ui::images::render_images;
use crate::ui::search_results::SearchResultsView;
use crate::ui::{self, note::NoteOptions, Preview, PreviewConfig};
use crate::Result;
@@ -13,7 +14,7 @@ use egui::{vec2, Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer};
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
use nostrdb::{Ndb, Transaction};
use notedeck::{ImageCache, NoteCache};
use notedeck::{supported_mime_hosted_at_url, Images, NoteCache};
use tracing::error;
use super::contents::render_note_preview;
@@ -22,7 +23,7 @@ pub struct PostView<'a> {
ndb: &'a Ndb,
draft: &'a mut Draft,
post_type: PostType,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
note_cache: &'a mut NoteCache,
poster: FilledKeypair<'a>,
id_source: Option<egui::Id>,
@@ -88,7 +89,7 @@ impl<'a> PostView<'a> {
ndb: &'a Ndb,
draft: &'a mut Draft,
post_type: PostType,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
note_cache: &'a mut NoteCache,
poster: FilledKeypair<'a>,
inner_rect: egui::Rect,
@@ -384,49 +385,59 @@ impl<'a> PostView<'a> {
} else {
(300, 300)
};
let m_cached_promise = self.img_cache.map().get(&media.url);
if m_cached_promise.is_none() {
let promise = fetch_img(
if let Some(cache_type) =
supported_mime_hosted_at_url(&mut self.img_cache.urls, &media.url)
{
render_images(
ui,
self.img_cache,
ui.ctx(),
&media.url,
crate::images::ImageType::Content(width, height),
cache_type,
|ui| {
ui.spinner();
},
|_, e| {
self.draft.upload_errors.push(e.to_string());
error!("{e}");
},
|ui, url, renderable_media, gifs| {
let media_size = vec2(width as f32, height as f32);
let max_size = vec2(300.0, 300.0);
let size = if media_size.x > max_size.x || media_size.y > max_size.y {
max_size
} else {
media_size
};
let texture_handle = handle_repaint(
ui,
retrieve_latest_texture(url, gifs, renderable_media),
);
let img_resp = ui.add(
egui::Image::new(texture_handle)
.max_size(size)
.rounding(12.0),
);
let remove_button_rect = {
let top_left = img_resp.rect.left_top();
let spacing = 13.0;
let center = Pos2::new(top_left.x + spacing, top_left.y + spacing);
egui::Rect::from_center_size(center, egui::vec2(26.0, 26.0))
};
if show_remove_upload_button(ui, remove_button_rect).clicked() {
to_remove.push(i);
}
ui.advance_cursor_after_rect(img_resp.rect);
},
);
self.img_cache
.map_mut()
.insert(media.url.to_owned(), promise);
}
match self.img_cache.map()[&media.url].ready() {
Some(Ok(texture)) => {
let media_size = vec2(width as f32, height as f32);
let max_size = vec2(300.0, 300.0);
let size = if media_size.x > max_size.x || media_size.y > max_size.y {
max_size
} else {
media_size
};
let img_resp = ui.add(egui::Image::new(texture).max_size(size).rounding(12.0));
let remove_button_rect = {
let top_left = img_resp.rect.left_top();
let spacing = 13.0;
let center = Pos2::new(top_left.x + spacing, top_left.y + spacing);
egui::Rect::from_center_size(center, egui::vec2(26.0, 26.0))
};
if show_remove_upload_button(ui, remove_button_rect).clicked() {
to_remove.push(i);
}
ui.advance_cursor_after_rect(img_resp.rect);
}
Some(Err(e)) => {
self.draft.upload_errors.push(e.to_string());
error!("{e}");
}
None => {
ui.spinner();
}
} else {
self.draft
.upload_errors
.push("Uploaded media is not supported.".to_owned());
error!("Unsupported mime type at url: {}", &media.url);
}
}
to_remove.reverse();

View File

@@ -1,6 +1,6 @@
use enostr::{FilledKeypair, NoteId};
use nostrdb::Ndb;
use notedeck::{ImageCache, NoteCache};
use notedeck::{Images, NoteCache};
use crate::{
draft::Draft,
@@ -13,7 +13,7 @@ pub struct QuoteRepostView<'a> {
ndb: &'a Ndb,
poster: FilledKeypair<'a>,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
draft: &'a mut Draft,
quoting_note: &'a nostrdb::Note<'a>,
id_source: Option<egui::Id>,
@@ -27,7 +27,7 @@ impl<'a> QuoteRepostView<'a> {
ndb: &'a Ndb,
poster: FilledKeypair<'a>,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
draft: &'a mut Draft,
quoting_note: &'a nostrdb::Note<'a>,
inner_rect: egui::Rect,

View File

@@ -4,13 +4,13 @@ use crate::ui::note::{NoteOptions, PostResponse, PostType};
use enostr::{FilledKeypair, NoteId};
use nostrdb::Ndb;
use notedeck::{ImageCache, NoteCache};
use notedeck::{Images, NoteCache};
pub struct PostReplyView<'a> {
ndb: &'a Ndb,
poster: FilledKeypair<'a>,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
draft: &'a mut Draft,
note: &'a nostrdb::Note<'a>,
id_source: Option<egui::Id>,
@@ -25,7 +25,7 @@ impl<'a> PostReplyView<'a> {
poster: FilledKeypair<'a>,
draft: &'a mut Draft,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
note: &'a nostrdb::Note<'a>,
inner_rect: egui::Rect,
note_options: NoteOptions,

View File

@@ -4,7 +4,7 @@ use crate::{
};
use egui::{Label, RichText, Sense};
use nostrdb::{Ndb, Note, NoteReply, Transaction};
use notedeck::{ImageCache, NoteCache};
use notedeck::{Images, NoteCache};
#[must_use = "Please handle the resulting note action"]
pub fn reply_desc(
@@ -12,7 +12,7 @@ pub fn reply_desc(
txn: &Transaction,
note_reply: &NoteReply,
ndb: &Ndb,
img_cache: &mut ImageCache,
img_cache: &mut Images,
note_cache: &mut NoteCache,
note_options: NoteOptions,
) -> Option<NoteAction> {
@@ -29,7 +29,7 @@ pub fn reply_desc(
// note link renderer helper
let note_link = |ui: &mut egui::Ui,
note_cache: &mut NoteCache,
img_cache: &mut ImageCache,
img_cache: &mut Images,
text: &str,
note: &Note<'_>| {
let r = ui.add(

View File

@@ -1,7 +1,7 @@
use core::f32;
use egui::{vec2, Button, Layout, Margin, RichText, Rounding, ScrollArea, TextEdit};
use notedeck::{ImageCache, NotedeckTextStyle};
use notedeck::{Images, NotedeckTextStyle};
use crate::{colors, profile_state::ProfileState};
@@ -9,11 +9,11 @@ use super::{banner, unwrap_profile_url, ProfilePic};
pub struct EditProfileView<'a> {
state: &'a mut ProfileState,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
}
impl<'a> EditProfileView<'a> {
pub fn new(state: &'a mut ProfileState, img_cache: &'a mut ImageCache) -> Self {
pub fn new(state: &'a mut ProfileState, img_cache: &'a mut Images) -> Self {
Self { state, img_cache }
}

View File

@@ -23,7 +23,7 @@ use crate::{
NostrName,
};
use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, NotedeckTextStyle, UnknownIds};
use notedeck::{Accounts, Images, MuteFun, NoteCache, NotedeckTextStyle, UnknownIds};
pub struct ProfileView<'a> {
pubkey: &'a Pubkey,
@@ -33,7 +33,7 @@ pub struct ProfileView<'a> {
note_options: NoteOptions,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
unknown_ids: &'a mut UnknownIds,
is_muted: &'a MuteFun,
}
@@ -52,7 +52,7 @@ impl<'a> ProfileView<'a> {
timeline_cache: &'a mut TimelineCache,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
unknown_ids: &'a mut UnknownIds,
is_muted: &'a MuteFun,
note_options: NoteOptions,

View File

@@ -1,13 +1,15 @@
use crate::gif::{handle_repaint, retrieve_latest_texture};
use crate::images::ImageType;
use crate::ui::images::render_images;
use crate::ui::{Preview, PreviewConfig};
use egui::{vec2, Sense, Stroke, TextureHandle};
use nostrdb::{Ndb, Transaction};
use tracing::info;
use notedeck::{AppContext, ImageCache};
use notedeck::{supported_mime_hosted_at_url, AppContext, Images};
pub struct ProfilePic<'cache, 'url> {
cache: &'cache mut ImageCache,
cache: &'cache mut Images,
url: &'url str,
size: f32,
border: Option<Stroke>,
@@ -20,7 +22,7 @@ impl egui::Widget for ProfilePic<'_, '_> {
}
impl<'cache, 'url> ProfilePic<'cache, 'url> {
pub fn new(cache: &'cache mut ImageCache, url: &'url str) -> Self {
pub fn new(cache: &'cache mut Images, url: &'url str) -> Self {
let size = Self::default_size();
ProfilePic {
cache,
@@ -35,7 +37,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
}
pub fn from_profile(
cache: &'cache mut ImageCache,
cache: &'cache mut Images,
profile: &nostrdb::ProfileRecord<'url>,
) -> Option<Self> {
profile
@@ -80,7 +82,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
fn render_pfp(
ui: &mut egui::Ui,
img_cache: &mut ImageCache,
img_cache: &mut Images,
url: &str,
ui_size: f32,
border: Option<Stroke>,
@@ -91,39 +93,27 @@ fn render_pfp(
// We will want to downsample these so it's not blurry on hi res displays
let img_size = 128u32;
let m_cached_promise = img_cache.map().get(url);
if m_cached_promise.is_none() {
let res = crate::images::fetch_img(img_cache, ui.ctx(), url, ImageType::Profile(img_size));
img_cache.map_mut().insert(url.to_owned(), res);
}
let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url)
.unwrap_or(notedeck::MediaCacheType::Image);
match img_cache.map()[url].ready() {
None => paint_circle(ui, ui_size, border),
// Failed to fetch profile!
Some(Err(_err)) => {
let m_failed_promise = img_cache.map().get(url);
if m_failed_promise.is_none() {
let no_pfp = crate::images::fetch_img(
img_cache,
ui.ctx(),
ProfilePic::no_pfp_url(),
ImageType::Profile(img_size),
);
img_cache.map_mut().insert(url.to_owned(), no_pfp);
}
match img_cache.map().get(url).unwrap().ready() {
None => paint_circle(ui, ui_size, border),
Some(Err(_e)) => {
//error!("Image load error: {:?}", e);
paint_circle(ui, ui_size, border)
}
Some(Ok(img)) => pfp_image(ui, img, ui_size, border),
}
}
Some(Ok(img)) => pfp_image(ui, img, ui_size, border),
}
render_images(
ui,
img_cache,
url,
ImageType::Profile(img_size),
cache_type,
|ui| {
paint_circle(ui, ui_size, border);
},
|ui, _| {
paint_circle(ui, ui_size, border);
},
|ui, url, renderable_media, gifs| {
let texture_handle =
handle_repaint(ui, retrieve_latest_texture(url, gifs, renderable_media));
pfp_image(ui, texture_handle, ui_size, border);
},
)
}
fn pfp_image(

View File

@@ -4,18 +4,18 @@ use egui::{Frame, Label, RichText, Widget};
use egui_extras::Size;
use nostrdb::ProfileRecord;
use notedeck::{ImageCache, NotedeckTextStyle, UserAccount};
use notedeck::{Images, NotedeckTextStyle, UserAccount};
use super::{about_section_widget, banner, display_name_widget, get_display_name, get_profile_url};
pub struct ProfilePreview<'a, 'cache> {
profile: &'a ProfileRecord<'a>,
cache: &'cache mut ImageCache,
cache: &'cache mut Images,
banner_height: Size,
}
impl<'a, 'cache> ProfilePreview<'a, 'cache> {
pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut ImageCache) -> Self {
pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut Images) -> Self {
let banner_height = Size::exact(80.0);
ProfilePreview {
profile,
@@ -69,14 +69,14 @@ impl egui::Widget for ProfilePreview<'_, '_> {
pub struct SimpleProfilePreview<'a, 'cache> {
profile: Option<&'a ProfileRecord<'a>>,
cache: &'cache mut ImageCache,
cache: &'cache mut Images,
is_nsec: bool,
}
impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
pub fn new(
profile: Option<&'a ProfileRecord<'a>>,
cache: &'cache mut ImageCache,
cache: &'cache mut Images,
is_nsec: bool,
) -> Self {
SimpleProfilePreview {

View File

@@ -1,6 +1,6 @@
use egui::{vec2, FontId, Pos2, Rect, ScrollArea, Vec2b};
use nostrdb::{Ndb, ProfileRecord, Transaction};
use notedeck::{fonts::get_font_size, ImageCache, NotedeckTextStyle};
use notedeck::{fonts::get_font_size, Images, NotedeckTextStyle};
use tracing::error;
use crate::{
@@ -13,13 +13,13 @@ use super::{profile::get_profile_url, ProfilePic};
pub struct SearchResultsView<'a> {
ndb: &'a Ndb,
txn: &'a Transaction,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
results: &'a Vec<&'a [u8; 32]>,
}
impl<'a> SearchResultsView<'a> {
pub fn new(
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
ndb: &'a Ndb,
txn: &'a Transaction,
results: &'a Vec<&'a [u8; 32]>,
@@ -84,7 +84,7 @@ impl<'a> SearchResultsView<'a> {
fn user_result<'a>(
profile: &'a ProfileRecord<'_>,
cache: &'a mut ImageCache,
cache: &'a mut Images,
index: usize,
width: f32,
) -> impl egui::Widget + 'a {

View File

@@ -15,7 +15,7 @@ use crate::{
support::Support,
};
use notedeck::{Accounts, ImageCache, NotedeckTextStyle, ThemeHandler, UserAccount};
use notedeck::{Accounts, Images, NotedeckTextStyle, ThemeHandler, UserAccount};
use super::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
@@ -29,7 +29,7 @@ static ICON_WIDTH: f32 = 40.0;
pub struct DesktopSidePanel<'a> {
ndb: &'a nostrdb::Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
selected_account: Option<&'a UserAccount>,
decks_cache: &'a DecksCache,
}
@@ -70,7 +70,7 @@ impl SidePanelResponse {
impl<'a> DesktopSidePanel<'a> {
pub fn new(
ndb: &'a nostrdb::Ndb,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
selected_account: Option<&'a UserAccount>,
decks_cache: &'a DecksCache,
) -> Self {

View File

@@ -5,7 +5,7 @@ use crate::{
};
use nostrdb::{Ndb, Transaction};
use notedeck::{ImageCache, MuteFun, NoteCache, RootNoteId, UnknownIds};
use notedeck::{Images, MuteFun, NoteCache, RootNoteId, UnknownIds};
use tracing::error;
use super::timeline::TimelineTabView;
@@ -15,7 +15,7 @@ pub struct ThreadView<'a> {
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
unknown_ids: &'a mut UnknownIds,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
selected_note_id: &'a [u8; 32],
note_options: NoteOptions,
id_source: egui::Id,
@@ -29,7 +29,7 @@ impl<'a> ThreadView<'a> {
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
unknown_ids: &'a mut UnknownIds,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
selected_note_id: &'a [u8; 32],
note_options: NoteOptions,
is_muted: &'a MuteFun,

View File

@@ -12,7 +12,7 @@ use egui::{vec2, Direction, Layout, Pos2, Stroke};
use egui_tabs::TabColor;
use nostrdb::{Ndb, Transaction};
use notedeck::note::root_note_id_from_selected_id;
use notedeck::{ImageCache, MuteFun, NoteCache};
use notedeck::{Images, MuteFun, NoteCache};
use tracing::{error, warn};
use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
@@ -22,19 +22,20 @@ pub struct TimelineView<'a> {
timeline_cache: &'a mut TimelineCache,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
note_options: NoteOptions,
reverse: bool,
is_muted: &'a MuteFun,
}
impl<'a> TimelineView<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
timeline_id: &'a TimelineKind,
timeline_cache: &'a mut TimelineCache,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
note_options: NoteOptions,
is_muted: &'a MuteFun,
) -> TimelineView<'a> {
@@ -78,7 +79,7 @@ fn timeline_ui(
timeline_id: &TimelineKind,
timeline_cache: &mut TimelineCache,
note_cache: &mut NoteCache,
img_cache: &mut ImageCache,
img_cache: &mut Images,
reversed: bool,
note_options: NoteOptions,
is_muted: &MuteFun,
@@ -321,7 +322,7 @@ pub struct TimelineTabView<'a> {
txn: &'a Transaction,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
is_muted: &'a MuteFun,
}
@@ -334,7 +335,7 @@ impl<'a> TimelineTabView<'a> {
txn: &'a Transaction,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
img_cache: &'a mut Images,
is_muted: &'a MuteFun,
) -> Self {
Self {