This commit is contained in:
Mike Dilger 2023-01-01 12:27:58 +13:00
parent 8585f72392
commit 14a54a5536
5 changed files with 179 additions and 14 deletions

View File

@ -26,6 +26,9 @@ use std::ops::DerefMut;
use std::{env, mem, thread};
use tracing_subscriber::filter::EnvFilter;
pub const AVATAR_SIZE: u32 = 48;
pub const AVATAR_SIZE_F32: f32 = 48.0;
fn main() -> Result<(), Error> {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info");

View File

@ -1,19 +1,31 @@
use crate::db::DbPerson;
use crate::error::Error;
use crate::globals::GLOBALS;
use nostr_types::{Metadata, PublicKeyHex, Unixtime};
use image::RgbaImage;
use nostr_types::{Metadata, PublicKeyHex, Unixtime, Url};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use tokio::task;
pub struct People {
people: HashMap<PublicKeyHex, DbPerson>,
// We fetch (with Fetcher), process, and temporarily hold avatars
// until the UI next asks for them, at which point we remove them
// and hand them over. This way we can do the work that takes
// longer and the UI can do as little work as possible.
avatars_temp: HashMap<PublicKeyHex, RgbaImage>,
avatars_pending_processing: HashSet<PublicKeyHex>,
avatars_failed: HashSet<PublicKeyHex>,
}
impl People {
pub fn new() -> People {
People {
people: HashMap::new(),
avatars_temp: HashMap::new(),
avatars_pending_processing: HashSet::new(),
avatars_failed: HashSet::new(),
}
}
@ -196,6 +208,89 @@ impl People {
v
}
// If returns Err, means you're never going to get it so stop trying.
pub fn get_avatar(&mut self, pubkeyhex: &PublicKeyHex) -> Result<Option<image::RgbaImage>, ()> {
// If we have it, hand it over (we won't need a copy anymore)
if let Some(th) = self.avatars_temp.remove(pubkeyhex) {
return Ok(Some(th));
}
// If it failed before, error out now
if self.avatars_failed.contains(pubkeyhex) {
return Err(());
}
// If it is pending processing, respond now
if self.avatars_pending_processing.contains(pubkeyhex) {
return Ok(None);
}
// Get the person this is about
let person = match self.people.get(pubkeyhex) {
Some(person) => person,
None => {
return Err(());
}
};
// Fail if they don't have a picture url
// FIXME: we could get metadata that sets this while we are running, so just failing for
// the duration of the client isn't quite right. But for now, retrying is taxing.
if person.picture.is_none() {
return Err(());
}
// FIXME: we could get metadata that sets this while we are running, so just failing for
// the duration of the client isn't quite right. But for now, retrying is taxing.
let url = Url::new(person.picture.as_ref().unwrap());
if !url.is_valid() {
return Err(());
}
match GLOBALS.fetcher.try_get(url) {
Ok(None) => Ok(None),
Ok(Some(bytes)) => {
// Finish this later
let apubkeyhex = pubkeyhex.to_owned();
tokio::spawn(async move {
let image = match image::load_from_memory(&bytes) {
// DynamicImage
Ok(di) => di,
Err(_) => {
let _ = GLOBALS
.people
.write()
.await
.avatars_failed
.insert(apubkeyhex.clone());
return;
}
};
let image = image.resize(
crate::AVATAR_SIZE,
crate::AVATAR_SIZE,
image::imageops::FilterType::Nearest,
); // DynamicImage
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
GLOBALS
.people
.write()
.await
.avatars_temp
.insert(apubkeyhex, image_buffer);
});
self.avatars_pending_processing.insert(pubkeyhex.to_owned());
Ok(None)
}
Err(e) => {
tracing::error!("{}", e);
self.avatars_failed.insert(pubkeyhex.to_owned());
Err(())
}
}
}
/// This is a 'just in case' the main code isn't keeping them in sync.
pub async fn populate_new_people() -> Result<(), Error> {
let sql = "INSERT or IGNORE INTO person (pubkey) SELECT DISTINCT pubkey FROM EVENT";

View File

@ -244,9 +244,20 @@ fn render_post(
}
// Avatar first
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &event.pubkey.into()) {
avatar
} else {
app.placeholder_avatar.clone()
};
if ui
.add(
Image::new(&app.placeholder_avatar, Vec2 { x: 36.0, y: 36.0 })
Image::new(
&avatar,
Vec2 {
x: crate::AVATAR_SIZE_F32,
y: crate::AVATAR_SIZE_F32,
},
)
.sense(Sense::click()),
)
.clicked()

View File

@ -16,6 +16,7 @@ use crate::settings::Settings;
use eframe::{egui, IconData, Theme};
use egui::{ColorImage, Context, ImageData, RichText, TextureHandle, TextureOptions, Ui};
use nostr_types::{Id, PublicKey, PublicKeyHex};
use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};
use zeroize::Zeroize;
@ -82,6 +83,8 @@ struct GossipUi {
person_view_pubkey: Option<PublicKeyHex>,
person_view_person: Option<DbPerson>,
person_view_name: Option<String>,
avatars: HashMap<PublicKeyHex, TextureHandle>,
failed_avatars: HashSet<PublicKeyHex>,
}
impl Drop for GossipUi {
@ -152,6 +155,8 @@ impl GossipUi {
person_view_pubkey: None,
person_view_person: None,
person_view_name: None,
avatars: HashMap::new(),
failed_avatars: HashSet::new(),
}
}
}
@ -249,4 +254,39 @@ impl GossipUi {
}
});
}
pub fn try_get_avatar(
&mut self,
ctx: &Context,
pubkeyhex: &PublicKeyHex,
) -> Option<TextureHandle> {
// Do not keep retrying if failed
if self.failed_avatars.contains(pubkeyhex) {
return None;
}
if let Some(th) = self.avatars.get(pubkeyhex) {
return Some(th.to_owned());
}
match GLOBALS.people.blocking_write().get_avatar(pubkeyhex) {
Err(_) => {
self.failed_avatars.insert(pubkeyhex.to_owned());
None
}
Ok(Some(rgbaimage)) => {
let size = [rgbaimage.width() as _, rgbaimage.height() as _];
let pixels = rgbaimage.as_flat_samples();
let texture_handle = ctx.load_texture(
pubkeyhex.0.clone(),
ImageData::Color(ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())),
TextureOptions::default(),
);
self.avatars
.insert(pubkeyhex.to_owned(), texture_handle.clone());
Some(texture_handle)
}
Ok(None) => None,
}
}
}

View File

@ -129,9 +129,20 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.horizontal(|ui| {
// Avatar first
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) {
avatar
} else {
app.placeholder_avatar.clone()
};
if ui
.add(
Image::new(&app.placeholder_avatar, Vec2 { x: 36.0, y: 36.0 })
Image::new(
&avatar,
Vec2 {
x: crate::AVATAR_SIZE_F32,
y: crate::AVATAR_SIZE_F32,
},
)
.sense(Sense::click()),
)
.clicked()
@ -157,9 +168,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
{
ui.label("ERROR");
} else {
let pubkeyhex = app.person_view_pubkey.as_ref().unwrap();
let person = app.person_view_person.as_ref().unwrap();
let name = app.person_view_name.as_ref().unwrap();
let pubkeyhex = app.person_view_pubkey.as_ref().unwrap().to_owned();
let person = app.person_view_person.as_ref().unwrap().to_owned();
let name = app.person_view_name.as_ref().unwrap().to_owned();
ui.add_space(24.0);
@ -167,11 +178,16 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.horizontal(|ui| {
// Avatar first
ui.image(&app.placeholder_avatar, Vec2 { x: 36.0, y: 36.0 });
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &pubkeyhex) {
avatar
} else {
app.placeholder_avatar.clone()
};
ui.image(&avatar, Vec2 { x: 36.0, y: 36.0 });
ui.vertical(|ui| {
ui.label(RichText::new(GossipUi::hex_pubkey_short(&person.pubkey)).weak());
GossipUi::render_person_name_line(ui, Some(person));
ui.label(RichText::new(GossipUi::hex_pubkey_short(&pubkeyhex)).weak());
GossipUi::render_person_name_line(ui, Some(&person));
});
});
@ -186,11 +202,11 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
#[allow(clippy::collapsible_else_if)]
if person.followed == 0 {
if ui.button("FOLLOW").clicked() {
GLOBALS.people.blocking_write().follow(pubkeyhex, true);
GLOBALS.people.blocking_write().follow(&pubkeyhex, true);
}
} else {
if ui.button("UNFOLLOW").clicked() {
GLOBALS.people.blocking_write().follow(pubkeyhex, false);
GLOBALS.people.blocking_write().follow(&pubkeyhex, false);
}
}
}