mirror of
https://github.com/damus-io/notecrumbs.git
synced 2024-09-29 18:10:43 +00:00
refactor data completion, add initial design from karnage
This commit is contained in:
parent
7a41982f14
commit
6cc648652a
53
src/error.rs
53
src/error.rs
@ -7,20 +7,65 @@ use tokio::sync::broadcast::error::RecvError;
|
||||
pub enum Error {
|
||||
Nip19(nip19::Error),
|
||||
Http(hyper::http::Error),
|
||||
Hyper(hyper::Error),
|
||||
Nostrdb(nostrdb::Error),
|
||||
NostrClient(nostr_sdk::client::Error),
|
||||
Recv(RecvError),
|
||||
Io(std::io::Error),
|
||||
Generic(String),
|
||||
Image(image::error::ImageError),
|
||||
Secp(nostr_sdk::secp256k1::Error),
|
||||
InvalidUri,
|
||||
NotFound,
|
||||
/// Profile picture is too big
|
||||
TooBig,
|
||||
InvalidNip19,
|
||||
InvalidProfilePic,
|
||||
SliceErr,
|
||||
}
|
||||
|
||||
impl From<image::error::ImageError> for Error {
|
||||
fn from(err: image::error::ImageError) -> Self {
|
||||
Error::Image(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<http::uri::InvalidUri> for Error {
|
||||
fn from(err: http::uri::InvalidUri) -> Self {
|
||||
Error::InvalidUri
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nostr_sdk::secp256k1::Error> for Error {
|
||||
fn from(err: nostr_sdk::secp256k1::Error) -> Self {
|
||||
Error::Secp(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(err: String) -> Self {
|
||||
Error::Generic(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RecvError> for Error {
|
||||
fn from(err: RecvError) -> Self {
|
||||
Error::Recv(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hyper::Error> for Error {
|
||||
fn from(err: hyper::Error) -> Self {
|
||||
Error::Hyper(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TryFromSliceError> for Error {
|
||||
fn from(_: TryFromSliceError) -> Self {
|
||||
Error::SliceErr
|
||||
@ -63,6 +108,14 @@ impl fmt::Display for Error {
|
||||
Error::Recv(e) => write!(f, "Recieve error: {}", e),
|
||||
Error::InvalidNip19 => write!(f, "Invalid nip19 object"),
|
||||
Error::SliceErr => write!(f, "Array slice error"),
|
||||
Error::TooBig => write!(f, "Profile picture is too big"),
|
||||
Error::InvalidProfilePic => write!(f, "Profile picture is corrupt"),
|
||||
Error::Image(err) => write!(f, "Image error: {}", err),
|
||||
Error::InvalidUri => write!(f, "Invalid url"),
|
||||
Error::Hyper(err) => write!(f, "Hyper error: {}", err),
|
||||
Error::Generic(err) => write!(f, "Generic error: {}", err),
|
||||
Error::Io(err) => write!(f, "Io error: {}", err),
|
||||
Error::Secp(err) => write!(f, "Signature error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// TODO: figure out the custom font situation
|
||||
|
||||
/*
|
||||
fn setup_fonts(font_data: &egui::FontData, ctx: &egui::Context) {
|
||||
pub fn setup_fonts(font_data: &egui::FontData, ctx: &egui::Context) {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
|
||||
// Install my own font (maybe supporting non-latin characters).
|
||||
@ -20,4 +19,3 @@ fn setup_fonts(font_data: &egui::FontData, ctx: &egui::Context) {
|
||||
// Tell egui to use these fonts:
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
*/
|
||||
|
138
src/main.rs
138
src/main.rs
@ -12,6 +12,7 @@ use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::render::NoteRenderData;
|
||||
use nostr_sdk::prelude::*;
|
||||
use nostrdb::{Config, Ndb, Transaction};
|
||||
use std::time::Duration;
|
||||
@ -19,28 +20,32 @@ use std::time::Duration;
|
||||
use lru::LruCache;
|
||||
|
||||
mod error;
|
||||
mod fonts;
|
||||
mod gradient;
|
||||
mod nip19;
|
||||
mod pfp;
|
||||
mod render;
|
||||
|
||||
pub enum Target {
|
||||
Profile(XOnlyPublicKey),
|
||||
Event(EventId),
|
||||
}
|
||||
|
||||
type ImageCache = LruCache<XOnlyPublicKey, egui::TextureHandle>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct Notecrumbs {
|
||||
ndb: Ndb,
|
||||
keys: Keys,
|
||||
font_data: egui::FontData,
|
||||
img_cache: Arc<ImageCache>,
|
||||
default_pfp: egui::ImageData,
|
||||
|
||||
/// How long do we wait for remote note requests
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<nostr_sdk::Event, Error> {
|
||||
pub struct FindNoteResult {
|
||||
note: Option<Event>,
|
||||
profile: Option<Event>,
|
||||
}
|
||||
|
||||
pub async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<FindNoteResult, Error> {
|
||||
let opts = Options::new().shutdown_on_drop(true);
|
||||
let client = Client::with_opts(&app.keys, opts);
|
||||
|
||||
@ -59,16 +64,28 @@ async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<nostr_sdk::Event,
|
||||
.req_events_of(filters.clone(), Some(app.timeout))
|
||||
.await;
|
||||
|
||||
let mut note: Option<Event> = None;
|
||||
let mut profile: Option<Event> = None;
|
||||
|
||||
loop {
|
||||
match client.notifications().recv().await? {
|
||||
RelayPoolNotification::Event(_url, ev) => {
|
||||
info!("got ev: {:?}", ev);
|
||||
return Ok(ev);
|
||||
debug!("got event 1 {:?}", ev);
|
||||
note = Some(ev);
|
||||
return Ok(FindNoteResult { note, profile });
|
||||
}
|
||||
RelayPoolNotification::RelayStatus { .. } => continue,
|
||||
RelayPoolNotification::Message(_url, msg) => match msg {
|
||||
RelayMessage::Event { event, .. } => return Ok(*event),
|
||||
RelayMessage::EndOfStoredEvents(_) => return Err(Error::NotFound),
|
||||
RelayMessage::Event { event, .. } => {
|
||||
if event.kind == Kind::Metadata {
|
||||
debug!("got profile {:?}", event);
|
||||
profile = Some(*event);
|
||||
} else {
|
||||
debug!("got event {:?}", event);
|
||||
note = Some(*event);
|
||||
}
|
||||
}
|
||||
RelayMessage::EndOfStoredEvents(_) => return Ok(FindNoteResult { note, profile }),
|
||||
_ => continue,
|
||||
},
|
||||
RelayPoolNotification::Stop | RelayPoolNotification::Shutdown => {
|
||||
@ -91,66 +108,22 @@ async fn serve(
|
||||
}
|
||||
};
|
||||
|
||||
let target = match nip19::to_target(&nip19) {
|
||||
Some(target) => target,
|
||||
None => {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Full::new(Bytes::from("\n")))?)
|
||||
}
|
||||
};
|
||||
|
||||
let content = {
|
||||
let mut txn = Transaction::new(&app.ndb)?;
|
||||
match target {
|
||||
Target::Profile(pk) => app
|
||||
.ndb
|
||||
.get_profile_by_pubkey(&mut txn, &pk.serialize())
|
||||
.and_then(|n| {
|
||||
info!("profile cache hit {:?}", nip19);
|
||||
Ok(n.record
|
||||
.profile()
|
||||
.ok_or(nostrdb::Error::NotFound)?
|
||||
.name()
|
||||
.ok_or(nostrdb::Error::NotFound)?
|
||||
.to_string())
|
||||
}),
|
||||
Target::Event(evid) => app
|
||||
.ndb
|
||||
.get_note_by_id(&mut txn, evid.as_bytes().try_into()?)
|
||||
.map(|n| {
|
||||
info!("event cache hit {:?}", nip19);
|
||||
n.content().to_string()
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
let content = match content {
|
||||
Ok(content) => content,
|
||||
Err(nostrdb::Error::NotFound) => {
|
||||
debug!("Finding {:?}", nip19);
|
||||
match find_note(app, &nip19).await {
|
||||
Ok(note) => {
|
||||
let _ = app
|
||||
.ndb
|
||||
.process_event(&json!(["EVENT", "s", note]).to_string());
|
||||
note.content
|
||||
}
|
||||
Err(_err) => {
|
||||
return Ok(Response::builder().status(StatusCode::NOT_FOUND).body(
|
||||
Full::new(Bytes::from(format!("noteid {:?} not found\n", nip19))),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
// render_data is always returned, it just might be empty
|
||||
let partial_render_data = match render::get_render_data(&app, &nip19) {
|
||||
Err(err) => {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Full::new(Bytes::from(format!("{}\n", err))))?);
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Full::new(Bytes::from(
|
||||
"nsecs are not supported, what were you thinking!?\n",
|
||||
)))?);
|
||||
}
|
||||
Ok(render_data) => render_data,
|
||||
};
|
||||
|
||||
let data = render::render_note(&app, &content);
|
||||
// fetch extra data if we are missing it
|
||||
let render_data = partial_render_data.complete(&app, &nip19).await;
|
||||
|
||||
let data = render::render_note(&app, &render_data);
|
||||
|
||||
Ok(Response::builder()
|
||||
.header(header::CONTENT_TYPE, "image/png")
|
||||
@ -164,6 +137,33 @@ fn get_env_timeout() -> Duration {
|
||||
Duration::from_millis(timeout_ms)
|
||||
}
|
||||
|
||||
fn get_gradient() -> egui::ColorImage {
|
||||
use egui::{pos2, Color32, ColorImage};
|
||||
use gradient::Gradient;
|
||||
|
||||
//let gradient = Gradient::linear(Color32::LIGHT_GRAY, Color32::DARK_GRAY);
|
||||
let size = pfp::PFP_SIZE as usize;
|
||||
let radius = (pfp::PFP_SIZE as f32) / 2.0;
|
||||
let center = pos2(radius, radius);
|
||||
let start_color = Color32::from_rgb(0x1E, 0x55, 0xFF);
|
||||
let end_color = Color32::from_rgb(0xFA, 0x0D, 0xD4);
|
||||
|
||||
let gradient = Gradient::radial_alpha_gradient(center, radius, start_color, end_color);
|
||||
let pixels = gradient.to_pixel_row();
|
||||
|
||||
assert_eq!(pixels.len(), size * size);
|
||||
ColorImage {
|
||||
size: [size, size],
|
||||
pixels,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_default_pfp() -> egui::ColorImage {
|
||||
let img = std::fs::read("assets/default_pfp_2.png").expect("default pfp missing");
|
||||
let mut dyn_image = image::load_from_memory(&img).expect("failed to load default pfp");
|
||||
pfp::process_pfp_bitmap(&mut dyn_image)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
env_logger::init();
|
||||
@ -181,11 +181,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let keys = Keys::generate();
|
||||
let timeout = get_env_timeout();
|
||||
let img_cache = Arc::new(LruCache::new(std::num::NonZeroUsize::new(64).unwrap()));
|
||||
let default_pfp = egui::ImageData::Color(Arc::new(get_default_pfp()));
|
||||
//let default_pfp = egui::ImageData::Color(get_gradient());
|
||||
let font_data = egui::FontData::from_static(include_bytes!("../fonts/NotoSans-Regular.ttf"));
|
||||
|
||||
let app = Notecrumbs {
|
||||
ndb,
|
||||
keys,
|
||||
timeout,
|
||||
img_cache,
|
||||
font_data,
|
||||
default_pfp,
|
||||
};
|
||||
|
||||
// We start a loop to continuously accept incoming connections
|
||||
|
10
src/nip19.rs
10
src/nip19.rs
@ -3,16 +3,6 @@ use crate::Target;
|
||||
use nostr_sdk::nips::nip19::Nip19;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
pub fn to_target(nip19: &Nip19) -> Option<Target> {
|
||||
match nip19 {
|
||||
Nip19::Event(ev) => Some(Target::Event(ev.event_id)),
|
||||
Nip19::EventId(evid) => Some(Target::Event(*evid)),
|
||||
Nip19::Profile(prof) => Some(Target::Profile(prof.public_key)),
|
||||
Nip19::Pubkey(pk) => Some(Target::Profile(*pk)),
|
||||
Nip19::Secret(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_filters(nip19: &Nip19) -> Result<Vec<Filter>, Error> {
|
||||
match nip19 {
|
||||
Nip19::Event(ev) => {
|
||||
|
97
src/pfp.rs
97
src/pfp.rs
@ -1,6 +1,11 @@
|
||||
use crate::Error;
|
||||
use bytes::Bytes;
|
||||
use egui::{Color32, ColorImage};
|
||||
use hyper::body::Incoming;
|
||||
use image::imageops::FilterType;
|
||||
|
||||
pub const PFP_SIZE: u32 = 64;
|
||||
|
||||
// Thank to gossip for this one!
|
||||
pub fn round_image(image: &mut ColorImage) {
|
||||
#[cfg(feature = "profiling")]
|
||||
@ -49,10 +54,12 @@ pub fn round_image(image: &mut ColorImage) {
|
||||
}
|
||||
}
|
||||
|
||||
fn process_pfp_bitmap(size: u32, image: &mut image::DynamicImage) -> ColorImage {
|
||||
pub fn process_pfp_bitmap(image: &mut image::DynamicImage) -> ColorImage {
|
||||
#[cfg(features = "profiling")]
|
||||
puffin::profile_function!();
|
||||
|
||||
let size = PFP_SIZE;
|
||||
|
||||
// Crop square
|
||||
let smaller = image.width().min(image.height());
|
||||
|
||||
@ -75,3 +82,91 @@ fn process_pfp_bitmap(size: u32, image: &mut image::DynamicImage) -> ColorImage
|
||||
round_image(&mut color_image);
|
||||
color_image
|
||||
}
|
||||
|
||||
async fn fetch_url(url: &str) -> Result<(Vec<u8>, hyper::Response<Incoming>), Error> {
|
||||
use http_body_util::BodyExt;
|
||||
use http_body_util::Empty;
|
||||
use hyper::Request;
|
||||
use hyper_util::rt::tokio::TokioIo;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
let mut data: Vec<u8> = vec![];
|
||||
let url = url.parse::<hyper::Uri>()?;
|
||||
let host = url.host().expect("uri has no host");
|
||||
let port = url.port_u16().unwrap_or(80);
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let io = TokioIo::new(stream);
|
||||
|
||||
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?;
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(err) = conn.await {
|
||||
println!("Connection failed: {:?}", err);
|
||||
}
|
||||
});
|
||||
|
||||
let authority = url.authority().unwrap().clone();
|
||||
|
||||
let req = Request::builder()
|
||||
.uri(url)
|
||||
.header(hyper::header::HOST, authority.as_str())
|
||||
.body(Empty::<Bytes>::new())?;
|
||||
|
||||
let mut res: hyper::Response<Incoming> = sender.send_request(req).await?;
|
||||
|
||||
// Stream the body, writing each chunk to stdout as we get it
|
||||
// (instead of buffering and printing at the end).
|
||||
while let Some(next) = res.frame().await {
|
||||
let frame = next?;
|
||||
if let Some(chunk) = frame.data_ref() {
|
||||
if data.len() + chunk.len() > 52428800
|
||||
/* 50 MiB */
|
||||
{
|
||||
return Err(Error::TooBig);
|
||||
}
|
||||
data.extend(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((data, res))
|
||||
}
|
||||
|
||||
pub async fn fetch_pfp(url: &str) -> Result<ColorImage, Error> {
|
||||
let (data, res) = fetch_url(url).await?;
|
||||
parse_img_response(data, res)
|
||||
}
|
||||
|
||||
fn parse_img_response(
|
||||
data: Vec<u8>,
|
||||
response: hyper::Response<Incoming>,
|
||||
) -> Result<ColorImage, Error> {
|
||||
use egui_extras::image::FitTo;
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_function!();
|
||||
|
||||
let content_type = response.headers()["content-type"]
|
||||
.to_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
let size = PFP_SIZE;
|
||||
|
||||
if content_type.starts_with("image/svg") {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_scope!("load_svg");
|
||||
|
||||
let mut color_image = egui_extras::image::load_svg_bytes_with_size(
|
||||
&data,
|
||||
FitTo::Size(size as u32, size as u32),
|
||||
)?;
|
||||
round_image(&mut color_image);
|
||||
Ok(color_image)
|
||||
} else if content_type.starts_with("image/") {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_scope!("load_from_memory");
|
||||
let mut dyn_image = image::load_from_memory(&data)?;
|
||||
Ok(process_pfp_bitmap(&mut dyn_image))
|
||||
} else {
|
||||
Err(Error::InvalidProfilePic)
|
||||
}
|
||||
}
|
||||
|
431
src/render.rs
431
src/render.rs
@ -1,30 +1,423 @@
|
||||
struct ProfileRenderData {}
|
||||
use crate::{fonts, Error, Notecrumbs};
|
||||
use egui::{Color32, ColorImage, FontId, RichText, Visuals};
|
||||
use log::{debug, info, warn};
|
||||
use nostr_sdk::nips::nip19::Nip19;
|
||||
use nostr_sdk::prelude::*;
|
||||
use nostrdb::{Note, Transaction};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Notecrumbs;
|
||||
impl ProfileRenderData {
|
||||
pub fn default(pfp: egui::ImageData) -> Self {
|
||||
ProfileRenderData {
|
||||
name: "nostrich".to_string(),
|
||||
display_name: None,
|
||||
about: "A am a nosy nostrich".to_string(),
|
||||
pfp: pfp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NoteRenderData {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NoteData {
|
||||
content: String,
|
||||
}
|
||||
|
||||
pub struct ProfileRenderData {
|
||||
name: String,
|
||||
display_name: Option<String>,
|
||||
about: String,
|
||||
pfp: egui::ImageData,
|
||||
}
|
||||
|
||||
pub struct NoteRenderData {
|
||||
note: NoteData,
|
||||
profile: ProfileRenderData,
|
||||
}
|
||||
|
||||
enum RenderData {
|
||||
Note(NoteRenderData),
|
||||
pub struct PartialNoteRenderData {
|
||||
note: Option<NoteData>,
|
||||
profile: Option<ProfileRenderData>,
|
||||
}
|
||||
|
||||
fn note_ui(app: &Notecrumbs, ctx: &egui::Context, content: &str) {
|
||||
pub enum PartialRenderData {
|
||||
Note(PartialNoteRenderData),
|
||||
Profile(Option<ProfileRenderData>),
|
||||
}
|
||||
|
||||
pub enum RenderData {
|
||||
Note(NoteRenderData),
|
||||
Profile(ProfileRenderData),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EventSource {
|
||||
Nip19(Nip19Event),
|
||||
Id(EventId),
|
||||
}
|
||||
|
||||
impl EventSource {
|
||||
fn id(&self) -> EventId {
|
||||
match self {
|
||||
EventSource::Nip19(ev) => ev.event_id,
|
||||
EventSource::Id(id) => *id,
|
||||
}
|
||||
}
|
||||
|
||||
fn author(&self) -> Option<XOnlyPublicKey> {
|
||||
match self {
|
||||
EventSource::Nip19(ev) => ev.author,
|
||||
EventSource::Id(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Nip19Event> for EventSource {
|
||||
fn from(event: Nip19Event) -> EventSource {
|
||||
EventSource::Nip19(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventId> for EventSource {
|
||||
fn from(event_id: EventId) -> EventSource {
|
||||
EventSource::Id(event_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl NoteData {
|
||||
fn default() -> Self {
|
||||
let content = "".to_string();
|
||||
NoteData { content }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialRenderData {
|
||||
pub async fn complete(self, app: &Notecrumbs, nip19: &Nip19) -> RenderData {
|
||||
match self {
|
||||
PartialRenderData::Note(partial) => {
|
||||
RenderData::Note(partial.complete(app, nip19).await)
|
||||
}
|
||||
|
||||
PartialRenderData::Profile(Some(profile)) => RenderData::Profile(profile),
|
||||
|
||||
PartialRenderData::Profile(None) => {
|
||||
warn!("TODO: implement profile data completion");
|
||||
RenderData::Profile(ProfileRenderData::default(app.default_pfp.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialNoteRenderData {
|
||||
pub async fn complete(self, app: &Notecrumbs, nip19: &Nip19) -> NoteRenderData {
|
||||
// we have everything, all done!
|
||||
match (self.note, self.profile) {
|
||||
(Some(note), Some(profile)) => {
|
||||
return NoteRenderData { note, profile };
|
||||
}
|
||||
|
||||
// Don't hold ourselves up on profile data for notes. We can spin
|
||||
// off a background task to find the profile though.
|
||||
(Some(note), None) => {
|
||||
warn!("TODO: spin off profile query when missing note profile");
|
||||
let profile = ProfileRenderData::default(app.default_pfp.clone());
|
||||
return NoteRenderData { note, profile };
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
debug!("Finding {:?}", nip19);
|
||||
|
||||
match crate::find_note(app, &nip19).await {
|
||||
Ok(note_res) => {
|
||||
let note = match note_res.note {
|
||||
Some(note) => {
|
||||
debug!("saving {:?} to nostrdb", ¬e);
|
||||
let _ = app
|
||||
.ndb
|
||||
.process_event(&json!(["EVENT", "s", note]).to_string());
|
||||
sdk_note_to_note_data(¬e)
|
||||
}
|
||||
None => NoteData::default(),
|
||||
};
|
||||
|
||||
let profile = match note_res.profile {
|
||||
Some(profile) => {
|
||||
debug!("saving profile to nostrdb: {:?}", &profile);
|
||||
let _ = app
|
||||
.ndb
|
||||
.process_event(&json!(["EVENT", "s", profile]).to_string());
|
||||
// TODO: wire profile to profile data, download pfp
|
||||
ProfileRenderData::default(app.default_pfp.clone())
|
||||
}
|
||||
None => ProfileRenderData::default(app.default_pfp.clone()),
|
||||
};
|
||||
|
||||
NoteRenderData { note, profile }
|
||||
}
|
||||
Err(_err) => {
|
||||
let note = NoteData::default();
|
||||
let profile = ProfileRenderData::default(app.default_pfp.clone());
|
||||
NoteRenderData { note, profile }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_profile_render_data(
|
||||
txn: &Transaction,
|
||||
app: &Notecrumbs,
|
||||
pubkey: &XOnlyPublicKey,
|
||||
) -> Result<ProfileRenderData, Error> {
|
||||
let profile = app.ndb.get_profile_by_pubkey(&txn, &pubkey.serialize())?;
|
||||
info!("profile cache hit {:?}", pubkey);
|
||||
|
||||
let profile = profile.record.profile().ok_or(nostrdb::Error::NotFound)?;
|
||||
let name = profile.name().unwrap_or("").to_string();
|
||||
let about = profile.about().unwrap_or("").to_string();
|
||||
let display_name = profile.display_name().as_ref().map(|a| a.to_string());
|
||||
let pfp = app.default_pfp.clone();
|
||||
|
||||
Ok(ProfileRenderData {
|
||||
name,
|
||||
pfp,
|
||||
about,
|
||||
display_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn ndb_note_to_data(note: &Note) -> NoteData {
|
||||
let content = note.content().to_string();
|
||||
NoteData { content }
|
||||
}
|
||||
|
||||
fn sdk_note_to_note_data(note: &Event) -> NoteData {
|
||||
let content = note.content.clone();
|
||||
NoteData { content }
|
||||
}
|
||||
|
||||
fn get_note_render_data(
|
||||
app: &Notecrumbs,
|
||||
source: &EventSource,
|
||||
) -> Result<PartialNoteRenderData, Error> {
|
||||
debug!("got here a");
|
||||
let txn = Transaction::new(&app.ndb)?;
|
||||
let m_note = app
|
||||
.ndb
|
||||
.get_note_by_id(&txn, source.id().as_bytes().try_into()?)
|
||||
.map_err(Error::Nostrdb);
|
||||
|
||||
debug!("note cached? {:?}", m_note);
|
||||
|
||||
// It's possible we have an author pk in an nevent, let's use it if we do.
|
||||
// This gives us the opportunity to load the profile picture earlier if we
|
||||
// have a cached profile
|
||||
let mut profile: Option<ProfileRenderData> = None;
|
||||
|
||||
let m_note_pk = m_note
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|n| XOnlyPublicKey::from_slice(n.pubkey()).ok());
|
||||
|
||||
let m_pk = m_note_pk.or(source.author());
|
||||
|
||||
// get profile render data if we can
|
||||
if let Some(pk) = m_pk {
|
||||
match get_profile_render_data(&txn, app, &pk) {
|
||||
Err(err) => warn!(
|
||||
"No profile found for {} for note {}: {}",
|
||||
&pk,
|
||||
&source.id(),
|
||||
err
|
||||
),
|
||||
Ok(record) => {
|
||||
debug!("profile record found for note");
|
||||
profile = Some(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let note = m_note.map(|n| ndb_note_to_data(&n)).ok();
|
||||
Ok(PartialNoteRenderData { profile, note })
|
||||
}
|
||||
|
||||
pub fn get_render_data(app: &Notecrumbs, target: &Nip19) -> Result<PartialRenderData, Error> {
|
||||
match target {
|
||||
Nip19::Profile(profile) => {
|
||||
let txn = Transaction::new(&app.ndb)?;
|
||||
Ok(PartialRenderData::Profile(
|
||||
get_profile_render_data(&txn, app, &profile.public_key).ok(),
|
||||
))
|
||||
}
|
||||
|
||||
Nip19::Pubkey(pk) => {
|
||||
let txn = Transaction::new(&app.ndb)?;
|
||||
Ok(PartialRenderData::Profile(
|
||||
get_profile_render_data(&txn, app, pk).ok(),
|
||||
))
|
||||
}
|
||||
|
||||
Nip19::Event(event) => Ok(PartialRenderData::Note(get_note_render_data(
|
||||
app,
|
||||
&EventSource::Nip19(event.clone()),
|
||||
)?)),
|
||||
|
||||
Nip19::EventId(evid) => Ok(PartialRenderData::Note(get_note_render_data(
|
||||
app,
|
||||
&EventSource::Id(*evid),
|
||||
)?)),
|
||||
|
||||
Nip19::Secret(_nsec) => Err(Error::InvalidNip19),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn floor_char_boundary(s: &str, index: usize) -> usize {
|
||||
if index >= s.len() {
|
||||
s.len()
|
||||
} else {
|
||||
let lower_bound = index.saturating_sub(3);
|
||||
let new_index = s.as_bytes()[lower_bound..=index]
|
||||
.iter()
|
||||
.rposition(|b| is_utf8_char_boundary(*b));
|
||||
|
||||
// SAFETY: we know that the character boundary will be within four bytes
|
||||
unsafe { lower_bound + new_index.unwrap_unchecked() }
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_utf8_char_boundary(c: u8) -> bool {
|
||||
// This is bit magic equivalent to: b < 128 || b >= 192
|
||||
(c as i8) >= -0x40
|
||||
}
|
||||
|
||||
fn ui_abbreviate_name(ui: &mut egui::Ui, name: &str, len: usize) {
|
||||
if name.len() > len {
|
||||
let closest = floor_char_boundary(name, len);
|
||||
heading(ui, &name[..closest]);
|
||||
heading(ui, "...");
|
||||
} else {
|
||||
heading(ui, name);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_username(app: &Notecrumbs, ui: &mut egui::Ui, profile: &ProfileRenderData) {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_function!();
|
||||
let name = format!("@{}", profile.name);
|
||||
ui.label(RichText::new(&name).size(30.0).color(Color32::DARK_GRAY));
|
||||
}
|
||||
|
||||
fn heading(ui: &mut egui::Ui, text: impl Into<RichText>) {
|
||||
ui.label(text.into().size(40.0));
|
||||
}
|
||||
|
||||
fn setup_visuals(font_data: &egui::FontData, ctx: &egui::Context) {
|
||||
let mut visuals = Visuals::dark();
|
||||
visuals.override_text_color = Some(Color32::WHITE);
|
||||
ctx.set_visuals(visuals);
|
||||
fonts::setup_fonts(font_data, ctx);
|
||||
}
|
||||
|
||||
fn wrapped_body(ui: &mut egui::Ui, text: &str) {
|
||||
use egui::text::{LayoutJob, TextFormat};
|
||||
|
||||
let format = TextFormat {
|
||||
font_id: FontId::proportional(40.0),
|
||||
color: Color32::WHITE,
|
||||
extra_letter_spacing: -1.0,
|
||||
line_height: Some(40.0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut job = LayoutJob::single_section(text.to_owned(), format);
|
||||
|
||||
job.justify = false;
|
||||
job.halign = egui::Align::LEFT;
|
||||
job.wrap = egui::text::TextWrapping {
|
||||
max_rows: 5,
|
||||
break_anywhere: false,
|
||||
overflow_character: Some('…'),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
ui.label(job);
|
||||
}
|
||||
|
||||
fn centered_layout() -> egui::Layout {
|
||||
use egui::{Align, Direction, Layout};
|
||||
|
||||
Layout {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: true,
|
||||
main_align: Align::Center,
|
||||
main_justify: true,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn note_ui(app: &Notecrumbs, ctx: &egui::Context, note: &NoteRenderData) {
|
||||
use egui::{FontId, Label, RichText, Rounding};
|
||||
|
||||
let pfp = ctx.load_texture("pfp", note.profile.pfp.clone(), Default::default());
|
||||
setup_visuals(&app.font_data, ctx);
|
||||
|
||||
let outer_margin = 40.0;
|
||||
let inner_margin = 100.0;
|
||||
let total_margin = outer_margin + inner_margin;
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::default().fill(Color32::from_rgb(0x43, 0x20, 0x62)))
|
||||
.show(&ctx, |ui| {
|
||||
egui::Frame::none()
|
||||
.fill(Color32::from_rgb(0x0F, 0x0F, 0x0F))
|
||||
.rounding(Rounding::same(20.0))
|
||||
.outer_margin(outer_margin)
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, |ui| {
|
||||
let desired_height = 630.0 - total_margin * 2.0;
|
||||
let desired_width = 1200.0 - total_margin * 2.0;
|
||||
let desired_size = egui::vec2(desired_width, desired_height);
|
||||
ui.set_min_height(desired_height); // Set minimum height for the container
|
||||
ui.set_min_width(desired_width); // Set minimum width for the container
|
||||
//
|
||||
ui.centered_and_justified(|ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
//ui.spacing_mut().item_spacing = egui::vec2(0.0, 0.0);
|
||||
|
||||
//ui.vertical(|ui| {
|
||||
wrapped_body(ui, ¬e.note.content);
|
||||
ui.horizontal(|ui| {
|
||||
ui.image(&pfp);
|
||||
render_username(app, ui, ¬e.profile);
|
||||
});
|
||||
//});
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn profile_ui(app: &Notecrumbs, ctx: &egui::Context, profile: &ProfileRenderData) {
|
||||
use egui::{FontId, RichText};
|
||||
|
||||
let pfp = ctx.load_texture("pfp", profile.pfp.clone(), Default::default());
|
||||
setup_visuals(&app.font_data, ctx);
|
||||
|
||||
egui::CentralPanel::default().show(&ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("✏").font(FontId::proportional(120.0)));
|
||||
ui.vertical(|ui| {
|
||||
ui.label(RichText::new(content).font(FontId::proportional(40.0)));
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.image(&pfp);
|
||||
render_username(app, ui, &profile);
|
||||
});
|
||||
})
|
||||
//body(ui, &profile.about);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_note(app: &Notecrumbs, content: &str) -> Vec<u8> {
|
||||
pub fn render_note(app: &Notecrumbs, render_data: &RenderData) -> Vec<u8> {
|
||||
use egui_skia::{rasterize, RasterizeOptions};
|
||||
use skia_safe::EncodedImageFormat;
|
||||
|
||||
@ -33,7 +426,19 @@ pub fn render_note(app: &Notecrumbs, content: &str) -> Vec<u8> {
|
||||
frames_before_screenshot: 1,
|
||||
};
|
||||
|
||||
let mut surface = rasterize((1200, 630), |ctx| note_ui(app, ctx, content), Some(options));
|
||||
let mut surface = match render_data {
|
||||
RenderData::Note(note_render_data) => rasterize(
|
||||
(1200, 630),
|
||||
|ctx| note_ui(app, ctx, note_render_data),
|
||||
Some(options),
|
||||
),
|
||||
|
||||
RenderData::Profile(profile_render_data) => rasterize(
|
||||
(1200, 630),
|
||||
|ctx| profile_ui(app, ctx, profile_render_data),
|
||||
Some(options),
|
||||
),
|
||||
};
|
||||
|
||||
surface
|
||||
.image_snapshot()
|
||||
|
Loading…
Reference in New Issue
Block a user