mirror of
https://github.com/mikedilger/gossip.git
synced 2024-09-19 19:46:50 +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 std::{env, mem, thread};
|
||||||
use tracing_subscriber::filter::EnvFilter;
|
use tracing_subscriber::filter::EnvFilter;
|
||||||
|
|
||||||
|
pub const AVATAR_SIZE: u32 = 48;
|
||||||
|
pub const AVATAR_SIZE_F32: f32 = 48.0;
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
fn main() -> Result<(), Error> {
|
||||||
if env::var("RUST_LOG").is_err() {
|
if env::var("RUST_LOG").is_err() {
|
||||||
env::set_var("RUST_LOG", "info");
|
env::set_var("RUST_LOG", "info");
|
||||||
|
@ -1,19 +1,31 @@
|
|||||||
use crate::db::DbPerson;
|
use crate::db::DbPerson;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::globals::GLOBALS;
|
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::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
pub struct People {
|
pub struct People {
|
||||||
people: HashMap<PublicKeyHex, DbPerson>,
|
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 {
|
impl People {
|
||||||
pub fn new() -> People {
|
pub fn new() -> People {
|
||||||
People {
|
People {
|
||||||
people: HashMap::new(),
|
people: HashMap::new(),
|
||||||
|
avatars_temp: HashMap::new(),
|
||||||
|
avatars_pending_processing: HashSet::new(),
|
||||||
|
avatars_failed: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,6 +208,89 @@ impl People {
|
|||||||
v
|
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.
|
/// This is a 'just in case' the main code isn't keeping them in sync.
|
||||||
pub async fn populate_new_people() -> Result<(), Error> {
|
pub async fn populate_new_people() -> Result<(), Error> {
|
||||||
let sql = "INSERT or IGNORE INTO person (pubkey) SELECT DISTINCT pubkey FROM EVENT";
|
let sql = "INSERT or IGNORE INTO person (pubkey) SELECT DISTINCT pubkey FROM EVENT";
|
||||||
|
@ -244,9 +244,20 @@ fn render_post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Avatar first
|
// Avatar first
|
||||||
|
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &event.pubkey.into()) {
|
||||||
|
avatar
|
||||||
|
} else {
|
||||||
|
app.placeholder_avatar.clone()
|
||||||
|
};
|
||||||
if ui
|
if ui
|
||||||
.add(
|
.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()),
|
.sense(Sense::click()),
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
|
@ -16,6 +16,7 @@ use crate::settings::Settings;
|
|||||||
use eframe::{egui, IconData, Theme};
|
use eframe::{egui, IconData, Theme};
|
||||||
use egui::{ColorImage, Context, ImageData, RichText, TextureHandle, TextureOptions, Ui};
|
use egui::{ColorImage, Context, ImageData, RichText, TextureHandle, TextureOptions, Ui};
|
||||||
use nostr_types::{Id, PublicKey, PublicKeyHex};
|
use nostr_types::{Id, PublicKey, PublicKeyHex};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
@ -82,6 +83,8 @@ struct GossipUi {
|
|||||||
person_view_pubkey: Option<PublicKeyHex>,
|
person_view_pubkey: Option<PublicKeyHex>,
|
||||||
person_view_person: Option<DbPerson>,
|
person_view_person: Option<DbPerson>,
|
||||||
person_view_name: Option<String>,
|
person_view_name: Option<String>,
|
||||||
|
avatars: HashMap<PublicKeyHex, TextureHandle>,
|
||||||
|
failed_avatars: HashSet<PublicKeyHex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for GossipUi {
|
impl Drop for GossipUi {
|
||||||
@ -152,6 +155,8 @@ impl GossipUi {
|
|||||||
person_view_pubkey: None,
|
person_view_pubkey: None,
|
||||||
person_view_person: None,
|
person_view_person: None,
|
||||||
person_view_name: 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| {
|
ui.horizontal(|ui| {
|
||||||
// Avatar first
|
// Avatar first
|
||||||
|
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) {
|
||||||
|
avatar
|
||||||
|
} else {
|
||||||
|
app.placeholder_avatar.clone()
|
||||||
|
};
|
||||||
if ui
|
if ui
|
||||||
.add(
|
.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()),
|
.sense(Sense::click()),
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
@ -157,9 +168,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
|
|||||||
{
|
{
|
||||||
ui.label("ERROR");
|
ui.label("ERROR");
|
||||||
} else {
|
} else {
|
||||||
let pubkeyhex = app.person_view_pubkey.as_ref().unwrap();
|
let pubkeyhex = app.person_view_pubkey.as_ref().unwrap().to_owned();
|
||||||
let person = app.person_view_person.as_ref().unwrap();
|
let person = app.person_view_person.as_ref().unwrap().to_owned();
|
||||||
let name = app.person_view_name.as_ref().unwrap();
|
let name = app.person_view_name.as_ref().unwrap().to_owned();
|
||||||
|
|
||||||
ui.add_space(24.0);
|
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| {
|
ui.horizontal(|ui| {
|
||||||
// Avatar first
|
// 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.vertical(|ui| {
|
||||||
ui.label(RichText::new(GossipUi::hex_pubkey_short(&person.pubkey)).weak());
|
ui.label(RichText::new(GossipUi::hex_pubkey_short(&pubkeyhex)).weak());
|
||||||
GossipUi::render_person_name_line(ui, Some(person));
|
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)]
|
#[allow(clippy::collapsible_else_if)]
|
||||||
if person.followed == 0 {
|
if person.followed == 0 {
|
||||||
if ui.button("FOLLOW").clicked() {
|
if ui.button("FOLLOW").clicked() {
|
||||||
GLOBALS.people.blocking_write().follow(pubkeyhex, true);
|
GLOBALS.people.blocking_write().follow(&pubkeyhex, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ui.button("UNFOLLOW").clicked() {
|
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