mirror of
https://github.com/mikedilger/gossip.git
synced 2024-09-19 11:43:43 +00:00
Avatars!
This commit is contained in:
parent
8585f72392
commit
14a54a5536
@ -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");
|
||||
|
@ -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";
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user