From 2dde3034a12049da9612fd646a1406f99796a858 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 1 Jan 2025 17:16:32 -0500 Subject: [PATCH 01/16] refactor profile Signed-off-by: kernelkind --- crates/notedeck_columns/src/ui/note/mod.rs | 2 +- crates/notedeck_columns/src/ui/profile/mod.rs | 115 +++++++++++++++++- .../src/ui/profile/preview.rs | 114 +---------------- 3 files changed, 117 insertions(+), 114 deletions(-) diff --git a/crates/notedeck_columns/src/ui/note/mod.rs b/crates/notedeck_columns/src/ui/note/mod.rs index e26e12a7..c7e1e38f 100644 --- a/crates/notedeck_columns/src/ui/note/mod.rs +++ b/crates/notedeck_columns/src/ui/note/mod.rs @@ -25,7 +25,7 @@ use enostr::{NoteId, Pubkey}; use nostrdb::{Ndb, Note, NoteKey, Transaction}; use notedeck::{CachedNote, ImageCache, NoteCache, NotedeckTextStyle}; -use super::profile::preview::{get_display_name, one_line_display_name_widget}; +use super::profile::{get_display_name, preview::one_line_display_name_widget}; pub struct NoteView<'a> { ndb: &'a Ndb, diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index a6ba48a6..00e69b0b 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -1,11 +1,13 @@ pub mod picture; pub mod preview; -use crate::notes_holder::NotesHolder; use crate::ui::note::NoteOptions; -use egui::{ScrollArea, Widget}; +use crate::{colors, images}; +use crate::{notes_holder::NotesHolder, DisplayName}; +use egui::load::TexturePoll; +use egui::{Label, RichText, ScrollArea, Sense, Widget}; use enostr::Pubkey; -use nostrdb::{Ndb, Transaction}; +use nostrdb::{Ndb, ProfileRecord, Transaction}; pub use picture::ProfilePic; pub use preview::ProfilePreview; use tracing::error; @@ -13,7 +15,7 @@ use tracing::error; use crate::{actionbar::NoteAction, notes_holder::NotesHolderStorage, profile::Profile}; use super::timeline::{tabs_ui, TimelineTabView}; -use notedeck::{ImageCache, MuteFun, NoteCache}; +use notedeck::{ImageCache, MuteFun, NoteCache, NotedeckTextStyle}; pub struct ProfileView<'a> { pubkey: &'a Pubkey, @@ -91,3 +93,108 @@ impl<'a> ProfileView<'a> { .inner } } + +fn display_name_widget( + display_name: DisplayName<'_>, + add_placeholder_space: bool, +) -> impl egui::Widget + '_ { + move |ui: &mut egui::Ui| match display_name { + DisplayName::One(n) => { + let name_response = ui.add( + Label::new(RichText::new(n).text_style(NotedeckTextStyle::Heading3.text_style())) + .selectable(false), + ); + if add_placeholder_space { + ui.add_space(16.0); + } + name_response + } + + DisplayName::Both { + display_name, + username, + } => { + ui.add( + Label::new( + RichText::new(display_name) + .text_style(NotedeckTextStyle::Heading3.text_style()), + ) + .selectable(false), + ); + + ui.add( + Label::new( + RichText::new(format!("@{}", username)) + .size(12.0) + .color(colors::MID_GRAY), + ) + .selectable(false), + ) + } + } +} + +pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { + if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { + url + } else { + ProfilePic::no_pfp_url() + } +} + +pub fn get_display_name<'a>(profile: Option<&ProfileRecord<'a>>) -> DisplayName<'a> { + if let Some(name) = profile.and_then(|p| crate::profile::get_profile_name(p)) { + name + } else { + DisplayName::One("??") + } +} + +fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b +where + 'b: 'a, +{ + move |ui: &mut egui::Ui| { + if let Some(about) = profile.record().profile().and_then(|p| p.about()) { + ui.label(about) + } else { + // need any Response so we dont need an Option + ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()) + } + } +} + +fn banner_texture( + ui: &mut egui::Ui, + profile: &ProfileRecord<'_>, +) -> Option { + // TODO: cache banner + let banner = profile.record().profile().and_then(|p| p.banner()); + + if let Some(banner) = banner { + let texture_load_res = + egui::Image::new(banner).load_for_size(ui.ctx(), ui.available_size()); + if let Ok(texture_poll) = texture_load_res { + match texture_poll { + TexturePoll::Pending { .. } => {} + TexturePoll::Ready { texture, .. } => return Some(texture), + } + } + } + + None +} + +fn banner(ui: &mut egui::Ui, profile: &ProfileRecord<'_>) -> egui::Response { + if let Some(texture) = banner_texture(ui, profile) { + images::aspect_fill( + ui, + Sense::hover(), + texture.id, + texture.size.x / texture.size.y, + ) + } else { + // TODO: default banner texture + ui.label("") + } +} diff --git a/crates/notedeck_columns/src/ui/profile/preview.rs b/crates/notedeck_columns/src/ui/profile/preview.rs index aba9966b..442f2fb3 100644 --- a/crates/notedeck_columns/src/ui/profile/preview.rs +++ b/crates/notedeck_columns/src/ui/profile/preview.rs @@ -1,13 +1,14 @@ use crate::ui::ProfilePic; -use crate::{colors, images, DisplayName}; -use egui::load::TexturePoll; -use egui::{Frame, Label, RichText, Sense, Widget}; +use crate::DisplayName; +use egui::{Frame, Label, RichText, Widget}; use egui_extras::Size; use enostr::{NoteId, Pubkey}; use nostrdb::{Ndb, ProfileRecord, Transaction}; use notedeck::{ImageCache, 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, @@ -28,41 +29,6 @@ impl<'a, 'cache> ProfilePreview<'a, 'cache> { self.banner_height = size; } - fn banner_texture( - ui: &mut egui::Ui, - profile: &ProfileRecord<'_>, - ) -> Option { - // TODO: cache banner - let banner = profile.record().profile().and_then(|p| p.banner()); - - if let Some(banner) = banner { - let texture_load_res = - egui::Image::new(banner).load_for_size(ui.ctx(), ui.available_size()); - if let Ok(texture_poll) = texture_load_res { - match texture_poll { - TexturePoll::Pending { .. } => {} - TexturePoll::Ready { texture, .. } => return Some(texture), - } - } - } - - None - } - - fn banner(ui: &mut egui::Ui, profile: &ProfileRecord<'_>) -> egui::Response { - if let Some(texture) = Self::banner_texture(ui, profile) { - images::aspect_fill( - ui, - Sense::hover(), - texture.id, - texture.size.x / texture.size.y, - ) - } else { - // TODO: default banner texture - ui.label("") - } - } - fn body(self, ui: &mut egui::Ui) { let padding = 12.0; crate::ui::padding(padding, ui, |ui| { @@ -89,7 +55,7 @@ impl egui::Widget for ProfilePreview<'_, '_> { fn ui(self, ui: &mut egui::Ui) -> egui::Response { ui.vertical(|ui| { ui.add_sized([ui.available_size().x, 80.0], |ui: &mut egui::Ui| { - ProfilePreview::banner(ui, self.profile) + banner(ui, self.profile) }); self.body(ui); @@ -183,22 +149,6 @@ mod previews { } } -pub fn get_display_name<'a>(profile: Option<&ProfileRecord<'a>>) -> DisplayName<'a> { - if let Some(name) = profile.and_then(|p| crate::profile::get_profile_name(p)) { - name - } else { - DisplayName::One("??") - } -} - -pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { - if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { - url - } else { - ProfilePic::no_pfp_url() - } -} - pub fn get_profile_url_owned(profile: Option>) -> &str { if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { url @@ -223,46 +173,6 @@ pub fn get_account_url<'a>( } } -fn display_name_widget( - display_name: DisplayName<'_>, - add_placeholder_space: bool, -) -> impl egui::Widget + '_ { - move |ui: &mut egui::Ui| match display_name { - DisplayName::One(n) => { - let name_response = ui.add( - Label::new(RichText::new(n).text_style(NotedeckTextStyle::Heading3.text_style())) - .selectable(false), - ); - if add_placeholder_space { - ui.add_space(16.0); - } - name_response - } - - DisplayName::Both { - display_name, - username, - } => { - ui.add( - Label::new( - RichText::new(display_name) - .text_style(NotedeckTextStyle::Heading3.text_style()), - ) - .selectable(false), - ); - - ui.add( - Label::new( - RichText::new(format!("@{}", username)) - .size(12.0) - .color(colors::MID_GRAY), - ) - .selectable(false), - ) - } - } -} - pub fn one_line_display_name_widget<'a>( visuals: &egui::Visuals, display_name: DisplayName<'a>, @@ -285,20 +195,6 @@ pub fn one_line_display_name_widget<'a>( } } -fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b -where - 'b: 'a, -{ - move |ui: &mut egui::Ui| { - if let Some(about) = profile.record().profile().and_then(|p| p.about()) { - ui.label(about) - } else { - // need any Response so we dont need an Option - ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()) - } - } -} - fn get_display_name_as_string<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { let display_name = get_display_name(profile); match display_name { From a99dad7e9ad1320cf8112dc079cc9ee64703e0e8 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 1 Jan 2025 17:16:32 -0500 Subject: [PATCH 02/16] profile body Signed-off-by: kernelkind --- crates/notedeck_columns/src/ui/profile/mod.rs | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index 00e69b0b..a7945996 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -5,7 +5,7 @@ use crate::ui::note::NoteOptions; use crate::{colors, images}; use crate::{notes_holder::NotesHolder, DisplayName}; use egui::load::TexturePoll; -use egui::{Label, RichText, ScrollArea, Sense, Widget}; +use egui::{Label, RichText, ScrollArea, Sense}; use enostr::Pubkey; use nostrdb::{Ndb, ProfileRecord, Transaction}; pub use picture::ProfilePic; @@ -56,7 +56,7 @@ impl<'a> ProfileView<'a> { .show(ui, |ui| { let txn = Transaction::new(self.ndb).expect("txn"); if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, self.pubkey.bytes()) { - ProfilePreview::new(&profile, self.img_cache).ui(ui); + self.profile_body(ui, profile); } let profile = self .profiles @@ -92,6 +92,43 @@ impl<'a> ProfileView<'a> { }) .inner } + + fn profile_body(&mut self, ui: &mut egui::Ui, profile: ProfileRecord<'_>) { + ui.vertical(|ui| { + ui.add_sized([ui.available_size().x, 120.0], |ui: &mut egui::Ui| { + banner(ui, &profile) + }); + + let padding = 12.0; + crate::ui::padding(padding, ui, |ui| { + let mut pfp_rect = ui.available_rect_before_wrap(); + let size = 80.0; + pfp_rect.set_width(size); + pfp_rect.set_height(size); + let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0)))); + + ui.put( + pfp_rect, + ProfilePic::new(self.img_cache, get_profile_url(Some(&profile))).size(size), + ); + ui.add(display_name_widget(get_display_name(Some(&profile)), false)); + ui.add(about_section_widget(&profile)); + + if let Some(website_url) = profile.record().profile().and_then(|p| p.website()) { + if ui + .label(RichText::new(website_url).color(colors::PINK)) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .interact(Sense::click()) + .clicked() + { + if let Err(e) = open::that(website_url) { + error!("Failed to open URL {} because: {}", website_url, e); + }; + } + } + }); + }); + } } fn display_name_widget( From df82e0804189d1fd80649c835fecc339c7331f74 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 1 Jan 2025 20:01:33 -0500 Subject: [PATCH 03/16] remove unused code Signed-off-by: kernelkind --- .../src/ui/profile/preview.rs | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/crates/notedeck_columns/src/ui/profile/preview.rs b/crates/notedeck_columns/src/ui/profile/preview.rs index 442f2fb3..6124ae27 100644 --- a/crates/notedeck_columns/src/ui/profile/preview.rs +++ b/crates/notedeck_columns/src/ui/profile/preview.rs @@ -194,31 +194,3 @@ pub fn one_line_display_name_widget<'a>( ), } } - -fn get_display_name_as_string<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { - let display_name = get_display_name(profile); - match display_name { - DisplayName::One(n) => n, - DisplayName::Both { display_name, .. } => display_name, - } -} - -pub fn get_profile_displayname_string<'a>(txn: &'a Transaction, ndb: &Ndb, pk: &Pubkey) -> &'a str { - let profile = ndb.get_profile_by_pubkey(txn, pk.bytes()).ok(); - get_display_name_as_string(profile.as_ref()) -} - -pub fn get_note_users_displayname_string<'a>( - txn: &'a Transaction, - ndb: &Ndb, - id: &NoteId, -) -> &'a str { - let note = ndb.get_note_by_id(txn, id.bytes()); - let profile = if let Ok(note) = note { - ndb.get_profile_by_pubkey(txn, note.pubkey()).ok() - } else { - None - }; - - get_display_name_as_string(profile.as_ref()) -} From a7cfe9bd37a48d183e5da29fe5d1cc8879824a6a Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 1 Jan 2025 21:03:11 -0500 Subject: [PATCH 04/16] refactor DisplayName -> NostrName Signed-off-by: kernelkind --- crates/notedeck_columns/src/lib.rs | 2 +- crates/notedeck_columns/src/profile.rs | 72 ++++++++++++------- crates/notedeck_columns/src/timeline/kind.rs | 5 +- crates/notedeck_columns/src/ui/mention.rs | 9 +-- crates/notedeck_columns/src/ui/note/mod.rs | 3 +- crates/notedeck_columns/src/ui/profile/mod.rs | 60 +++++++--------- .../src/ui/profile/preview.rs | 20 ++---- 7 files changed, 88 insertions(+), 83 deletions(-) diff --git a/crates/notedeck_columns/src/lib.rs b/crates/notedeck_columns/src/lib.rs index c670e664..06b1309f 100644 --- a/crates/notedeck_columns/src/lib.rs +++ b/crates/notedeck_columns/src/lib.rs @@ -42,6 +42,6 @@ pub mod storage; pub use app::Damus; pub use error::Error; -pub use profile::DisplayName; +pub use profile::NostrName; pub type Result = std::result::Result; diff --git a/crates/notedeck_columns/src/profile.rs b/crates/notedeck_columns/src/profile.rs index 644ffb1e..229ab957 100644 --- a/crates/notedeck_columns/src/profile.rs +++ b/crates/notedeck_columns/src/profile.rs @@ -9,20 +9,28 @@ use crate::{ timeline::{copy_notes_into_timeline, PubkeySource, Timeline, TimelineKind, TimelineTab}, }; -pub enum DisplayName<'a> { - One(&'a str), - - Both { - username: &'a str, - display_name: &'a str, - }, +pub struct NostrName<'a> { + pub username: Option<&'a str>, + pub display_name: Option<&'a str>, + pub nip05: Option<&'a str>, } -impl<'a> DisplayName<'a> { - pub fn username(&self) -> &'a str { - match self { - Self::One(n) => n, - Self::Both { username, .. } => username, +impl<'a> NostrName<'a> { + pub fn name(&self) -> &'a str { + if let Some(name) = self.username { + name + } else if let Some(name) = self.display_name { + name + } else { + self.nip05.unwrap_or("??") + } + } + + pub fn unknown() -> Self { + Self { + username: None, + display_name: None, + nip05: None, } } } @@ -31,19 +39,35 @@ fn is_empty(s: &str) -> bool { s.chars().all(|c| c.is_whitespace()) } -pub fn get_profile_name<'a>(record: &ProfileRecord<'a>) -> Option> { - let profile = record.record().profile()?; - let display_name = profile.display_name().filter(|n| !is_empty(n)); - let name = profile.name().filter(|n| !is_empty(n)); +pub fn get_display_name<'a>(record: Option<&ProfileRecord<'a>>) -> NostrName<'a> { + if let Some(record) = record { + if let Some(profile) = record.record().profile() { + let display_name = profile.display_name().filter(|n| !is_empty(n)); + let username = profile.name().filter(|n| !is_empty(n)); + let nip05 = if let Some(raw_nip05) = profile.nip05() { + if let Some(at_pos) = raw_nip05.find('@') { + if raw_nip05.starts_with('_') { + raw_nip05.get(at_pos + 1..) + } else { + Some(raw_nip05) + } + } else { + None + } + } else { + None + }; - match (display_name, name) { - (None, None) => None, - (Some(disp), None) => Some(DisplayName::One(disp)), - (None, Some(username)) => Some(DisplayName::One(username)), - (Some(display_name), Some(username)) => Some(DisplayName::Both { - display_name, - username, - }), + NostrName { + username, + display_name, + nip05, + } + } else { + NostrName::unknown() + } + } else { + NostrName::unknown() } } diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs index 446b877f..6eb9bd1f 100644 --- a/crates/notedeck_columns/src/timeline/kind.rs +++ b/crates/notedeck_columns/src/timeline/kind.rs @@ -241,10 +241,9 @@ impl<'a> TitleNeedsDb<'a> { let pubkey = pubkey_source.to_pubkey(deck_author); let profile = ndb.get_profile_by_pubkey(txn, pubkey); let m_name = profile - .ok() .as_ref() - .and_then(|p| crate::profile::get_profile_name(p)) - .map(|display_name| display_name.username()); + .ok() + .map(|p| crate::profile::get_display_name(Some(p)).name()); m_name.unwrap_or("Profile") } else { diff --git a/crates/notedeck_columns/src/ui/mention.rs b/crates/notedeck_columns/src/ui/mention.rs index dbaf987b..407c70c7 100644 --- a/crates/notedeck_columns/src/ui/mention.rs +++ b/crates/notedeck_columns/src/ui/mention.rs @@ -1,5 +1,5 @@ -use crate::actionbar::NoteAction; use crate::ui; +use crate::{actionbar::NoteAction, profile::get_display_name}; use egui::Sense; use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; @@ -79,12 +79,7 @@ fn mention_ui( ui.horizontal(|ui| { let profile = ndb.get_profile_by_pubkey(txn, pk).ok(); - let name: String = - if let Some(name) = profile.as_ref().and_then(crate::profile::get_profile_name) { - format!("@{}", name.username()) - } else { - "@???".to_string() - }; + let name: String = format!("@{}", get_display_name(profile.as_ref()).name()); let resp = ui.add( egui::Label::new(egui::RichText::new(name).color(link_color).size(size)) diff --git a/crates/notedeck_columns/src/ui/note/mod.rs b/crates/notedeck_columns/src/ui/note/mod.rs index c7e1e38f..dda30143 100644 --- a/crates/notedeck_columns/src/ui/note/mod.rs +++ b/crates/notedeck_columns/src/ui/note/mod.rs @@ -16,6 +16,7 @@ pub use reply_description::reply_desc; use crate::{ actionbar::NoteAction, + profile::get_display_name, ui::{self, View}, }; @@ -25,7 +26,7 @@ use enostr::{NoteId, Pubkey}; use nostrdb::{Ndb, Note, NoteKey, Transaction}; use notedeck::{CachedNote, ImageCache, NoteCache, NotedeckTextStyle}; -use super::profile::{get_display_name, preview::one_line_display_name_widget}; +use super::profile::preview::one_line_display_name_widget; pub struct NoteView<'a> { ndb: &'a Ndb, diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index a7945996..6632aa3d 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -1,9 +1,10 @@ pub mod picture; pub mod preview; +use crate::profile::get_display_name; use crate::ui::note::NoteOptions; use crate::{colors, images}; -use crate::{notes_holder::NotesHolder, DisplayName}; +use crate::{notes_holder::NotesHolder, NostrName}; use egui::load::TexturePoll; use egui::{Label, RichText, ScrollArea, Sense}; use enostr::Pubkey; @@ -131,43 +132,42 @@ impl<'a> ProfileView<'a> { } } -fn display_name_widget( - display_name: DisplayName<'_>, - add_placeholder_space: bool, -) -> impl egui::Widget + '_ { - move |ui: &mut egui::Ui| match display_name { - DisplayName::One(n) => { - let name_response = ui.add( - Label::new(RichText::new(n).text_style(NotedeckTextStyle::Heading3.text_style())) - .selectable(false), - ); - if add_placeholder_space { - ui.add_space(16.0); - } - name_response - } - - DisplayName::Both { - display_name, - username, - } => { +fn display_name_widget(name: NostrName<'_>, add_placeholder_space: bool) -> impl egui::Widget + '_ { + move |ui: &mut egui::Ui| -> egui::Response { + let disp_resp = name.display_name.map(|disp_name| { ui.add( Label::new( - RichText::new(display_name) - .text_style(NotedeckTextStyle::Heading3.text_style()), + RichText::new(disp_name).text_style(NotedeckTextStyle::Heading3.text_style()), ) .selectable(false), - ); - + ) + }); + let username_resp = name.username.map(|username| { ui.add( Label::new( RichText::new(format!("@{}", username)) - .size(12.0) + .size(16.0) .color(colors::MID_GRAY), ) .selectable(false), ) + }); + + let resp = if let Some(disp_resp) = disp_resp { + if let Some(username_resp) = username_resp { + username_resp + } else { + disp_resp + } + } else { + ui.add(Label::new(RichText::new(name.name()))) + }; + + if add_placeholder_space { + ui.add_space(16.0); } + + resp } } @@ -179,14 +179,6 @@ pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { } } -pub fn get_display_name<'a>(profile: Option<&ProfileRecord<'a>>) -> DisplayName<'a> { - if let Some(name) = profile.and_then(|p| crate::profile::get_profile_name(p)) { - name - } else { - DisplayName::One("??") - } -} - fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b where 'b: 'a, diff --git a/crates/notedeck_columns/src/ui/profile/preview.rs b/crates/notedeck_columns/src/ui/profile/preview.rs index 6124ae27..0befac3f 100644 --- a/crates/notedeck_columns/src/ui/profile/preview.rs +++ b/crates/notedeck_columns/src/ui/profile/preview.rs @@ -1,9 +1,8 @@ use crate::ui::ProfilePic; -use crate::DisplayName; +use crate::NostrName; use egui::{Frame, Label, RichText, Widget}; use egui_extras::Size; -use enostr::{NoteId, Pubkey}; -use nostrdb::{Ndb, ProfileRecord, Transaction}; +use nostrdb::ProfileRecord; use notedeck::{ImageCache, NotedeckTextStyle, UserAccount}; @@ -175,22 +174,17 @@ pub fn get_account_url<'a>( pub fn one_line_display_name_widget<'a>( visuals: &egui::Visuals, - display_name: DisplayName<'a>, + display_name: NostrName<'a>, style: NotedeckTextStyle, ) -> impl egui::Widget + 'a { let text_style = style.text_style(); let color = visuals.noninteractive().fg_stroke.color; - move |ui: &mut egui::Ui| match display_name { - DisplayName::One(n) => ui.label(RichText::new(n).text_style(text_style).color(color)), - - DisplayName::Both { - display_name, - username: _, - } => ui.label( - RichText::new(display_name) + move |ui: &mut egui::Ui| -> egui::Response { + ui.label( + RichText::new(display_name.name()) .text_style(text_style) .color(color), - ), + ) } } From 45d07cc432e0b7a365c72ffeebb8b01e196eefe8 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 1 Jan 2025 22:45:28 -0500 Subject: [PATCH 05/16] profile view improvements Signed-off-by: kernelkind --- assets/icons/key_4x.png | Bin 0 -> 1446 bytes assets/icons/links_4x.png | Bin 0 -> 1332 bytes assets/icons/verified_4x.png | Bin 0 -> 3376 bytes assets/icons/zap_4x.png | Bin 0 -> 1122 bytes crates/notedeck_columns/src/colors.rs | 1 + crates/notedeck_columns/src/ui/profile/mod.rs | 167 ++++++++++++++---- 6 files changed, 138 insertions(+), 30 deletions(-) create mode 100644 assets/icons/key_4x.png create mode 100644 assets/icons/links_4x.png create mode 100644 assets/icons/verified_4x.png create mode 100644 assets/icons/zap_4x.png diff --git a/assets/icons/key_4x.png b/assets/icons/key_4x.png new file mode 100644 index 0000000000000000000000000000000000000000..6c4f9d70ff39191f876d5c9c55b6d422806f3ce6 GIT binary patch literal 1446 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�IfDgX$Mg%vR7X&NFGU&twEFBF=f~&DGjws2k ztALE)z7Qn5AzY_2y(8#zCbe_P$wHJOQk6`g<;W`OiFWtx=>dQ}q1_=2m6qarm1 z;IT})s{l@rRg1_#erlTcieRJqZWM4Digb=IYC$uNB5|+0cKTqhQ$ci9T9N_G4#kzr zC87y}x@(n8U5Z@M0s_4SXoq z_y)h0OPhJAhpgUGMN4n*dYsUJYv86qfIhx&MATZ4fW0UbT`W@n305C+=!l-jp#>)X zCjDQ~V>gR|`B}-Ha$TCzX(VGeXyZ6;muN*b5ov3NJ4D4ErlNs22TEO!%WiG-?pFnv%Rgpgl>EYvF)$#h~}a=8@YbzIclT3W!Q&k@=_! z!#f$Cm+(9;>OngaH=3u%;>{-dw3konLhkZ>T5R~;?cJpOpOKE`I z6ecP|NUA8*vnP8!^oyHrXqT8OfKiI$>sGdf{!Fe5GpF@kV0AsmC9?yIOZhchmXI%1 z9!e=vy(Y>t(F9a`atsDlUsG42r`9>Y4f+Pc8sol@mpbQLz`ur=O57-GLw1iv!`GH0 z2l;&k$PW|x^V+JmOn)5bzLb-Bds*X9%~*Xmx#&P0o(jNz=RC|mxutn>{C8rw5iVQ z%{R5-GRIH=SI?V*c?RY&C+azQCaw2`IpQ%b4aViCw|sUUh8#l!jG-iK zHlILo;|HmfRLTdrlLJ-zzb}SCH772OWikQ601Z8X0gEug8108Zy_LOHL7L+jNifDN zDUE&k=Oa+vFPd3e?ts>s1~l|TR)$v3kpaGR$6W^lakG}`3|C{0b zTj6xCI3UVuDN>X6B=aT3$%D^qxpg;;IED@r{o~t54z?apl%Aw?s^uZVxo2-39bBRXK7euGkpoi~pfTNWa4#@n|7s8{ zyl~s+7B6T1%p0%m@G&KHxJi=DL&06o&tD=-`DlC?#(x1uQD=_bhjml{0000@~0drDELIAGL9O(c600d`2O+f$vv5yPg7|Gd)nm(a{K2Vk;{fCR_)TU%R)E1$)7);R!^MSz&&*H?JZ-YLg1 z%>kIs0;D*;u)O7@w$?cS)29xkI6h2oRxd}O4#G%(vW;Vos5_p2Qq~pMHo@TdKeT#w ze3+iNws(7bJ4gKvmFEJ}Zvr9IOY2%wAeQ3?#~(M;JF;OE{7I#?0BbnDgd;l+uCA_j zw93{3v~_%iSbbJ-P6M@$TCk?$OPtXwA=w0Dt2)8)r<8uY!Qy8djJrAfaB=B zTvZ>nj!6=ri{qCj7&4#Rd!@+*(rfkZGdcyjI(|u3?mx8Pd7p98bc`zU1vwnQ7??ub zF;1yoZn8GsUN;JW;7_Q#;}-#)a}t^Nhn+IPhFjKu@X*h6_r^kA;$)ML6?T=UT0QC} zFyy3_XjOpl(83AXF@ae{Px|`$`T)YKp49egw9)4SY8=NJ@Tf%aO{wjyP4J8=fc4@e zMbhI$$3LQ$r~*LAJG{)u4&PLn!|^2=rvNJov%4e%nifNHIle^Wf=EzwgcF!PJUlpj zk)!T>A>n=kLVKxkaC|kjmui2`f&?8?O?ZxC(45 z<$m=;3CEW(7XU$tE{@*x%lD-mU&34fzIBz#u_fl0?@Kwpg!w~Kr1a|2#Y;NAcs#Pj zDdnDjr8s_@dxjLO1rU3ma{vp+m#}_JP9547jxW(^XJ`$aR2bPL$>#VH)>B}+W&5m- zFJUTxmIG+yc6{+W$Pg_DkSO8!5|)q3syRTNQjRZSIRac_yJC(nVJSdL?-zA^2{Qqj za)3Z_$Cr3DQ^3OU#qT8}nl6D>jxS**04{;z(6)4Z2{QpO#5qX6TE+{mmX0rB`k$k` o(u2j0ad;-+XUa~{8*h~5FYJw7dx4G>CIA2c07*qoM6N<$f{PdjN&o-= literal 0 HcmV?d00001 diff --git a/crates/notedeck_columns/src/colors.rs b/crates/notedeck_columns/src/colors.rs index ef3b1d86..36f507bf 100644 --- a/crates/notedeck_columns/src/colors.rs +++ b/crates/notedeck_columns/src/colors.rs @@ -3,3 +3,4 @@ use egui::Color32; pub const ALMOST_WHITE: Color32 = Color32::from_rgb(0xFA, 0xFA, 0xFA); pub const MID_GRAY: Color32 = Color32::from_rgb(0xbd, 0xbd, 0xbd); pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9); +pub const TEAL: Color32 = Color32::from_rgb(0x77, 0xDC, 0xE1); diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index 6632aa3d..51070f02 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -6,7 +6,7 @@ use crate::ui::note::NoteOptions; use crate::{colors, images}; use crate::{notes_holder::NotesHolder, NostrName}; use egui::load::TexturePoll; -use egui::{Label, RichText, ScrollArea, Sense}; +use egui::{Label, RichText, Rounding, ScrollArea, Sense, Stroke}; use enostr::Pubkey; use nostrdb::{Ndb, ProfileRecord, Transaction}; pub use picture::ProfilePic; @@ -112,26 +112,116 @@ impl<'a> ProfileView<'a> { pfp_rect, ProfilePic::new(self.img_cache, get_profile_url(Some(&profile))).size(size), ); + + if ui.add(copy_key_widget(&pfp_rect)).clicked() { + ui.output_mut(|w| { + w.copied_text = if let Some(bech) = self.pubkey.to_bech() { + bech + } else { + error!("Could not convert Pubkey to bech"); + String::new() + } + }); + } + + ui.add_space(18.0); + ui.add(display_name_widget(get_display_name(Some(&profile)), false)); + + ui.add_space(8.0); + ui.add(about_section_widget(&profile)); - if let Some(website_url) = profile.record().profile().and_then(|p| p.website()) { - if ui - .label(RichText::new(website_url).color(colors::PINK)) - .on_hover_cursor(egui::CursorIcon::PointingHand) - .interact(Sense::click()) - .clicked() + ui.horizontal_wrapped(|ui| { + if let Some(website_url) = profile + .record() + .profile() + .and_then(|p| p.website()) + .filter(|s| !s.is_empty()) { - if let Err(e) = open::that(website_url) { - error!("Failed to open URL {} because: {}", website_url, e); - }; + handle_link(ui, website_url); } - } + + if let Some(lud16) = profile + .record() + .profile() + .and_then(|p| p.lud16()) + .filter(|s| !s.is_empty()) + { + handle_lud16(ui, lud16); + } + }); }); }); } } +fn handle_link(ui: &mut egui::Ui, website_url: &str) { + ui.image(egui::include_image!( + "../../../../../assets/icons/links_4x.png" + )); + if ui + .label(RichText::new(website_url).color(colors::PINK)) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .interact(Sense::click()) + .clicked() + { + if let Err(e) = open::that(website_url) { + error!("Failed to open URL {} because: {}", website_url, e); + }; + } +} + +fn handle_lud16(ui: &mut egui::Ui, lud16: &str) { + ui.image(egui::include_image!( + "../../../../../assets/icons/zap_4x.png" + )); + + let _ = ui.label(RichText::new(lud16).color(colors::PINK)); +} + +fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ { + |ui: &mut egui::Ui| -> egui::Response { + let painter = ui.painter(); + let copy_key_rect = painter.round_rect_to_pixels(egui::Rect::from_center_size( + pfp_rect.center_bottom(), + egui::vec2(48.0, 28.0), + )); + let resp = ui.interact( + copy_key_rect, + ui.id().with("custom_painter"), + Sense::click(), + ); + + let copy_key_rounding = Rounding::same(100.0); + let fill_color = if resp.hovered() { + ui.visuals().widgets.inactive.weak_bg_fill + } else { + ui.visuals().noninteractive().bg_stroke.color + }; + painter.rect_filled(copy_key_rect, copy_key_rounding, fill_color); + + let stroke_color = ui.visuals().widgets.inactive.weak_bg_fill; + painter.rect_stroke( + copy_key_rect.shrink(1.0), + copy_key_rounding, + Stroke::new(1.0, stroke_color), + ); + egui::Image::new(egui::include_image!( + "../../../../../assets/icons/key_4x.png" + )) + .paint_at( + ui, + painter.round_rect_to_pixels(egui::Rect::from_center_size( + copy_key_rect.center(), + egui::vec2(16.0, 16.0), + )), + ); + + resp + } +} + fn display_name_widget(name: NostrName<'_>, add_placeholder_space: bool) -> impl egui::Widget + '_ { move |ui: &mut egui::Ui| -> egui::Response { let disp_resp = name.display_name.map(|disp_name| { @@ -142,25 +232,40 @@ fn display_name_widget(name: NostrName<'_>, add_placeholder_space: bool) -> impl .selectable(false), ) }); - let username_resp = name.username.map(|username| { - ui.add( - Label::new( - RichText::new(format!("@{}", username)) - .size(16.0) - .color(colors::MID_GRAY), - ) - .selectable(false), - ) - }); - let resp = if let Some(disp_resp) = disp_resp { - if let Some(username_resp) = username_resp { - username_resp - } else { - disp_resp - } - } else { - ui.add(Label::new(RichText::new(name.name()))) + let (username_resp, nip05_resp) = ui + .horizontal(|ui| { + let username_resp = name.username.map(|username| { + ui.add( + Label::new( + RichText::new(format!("@{}", username)) + .size(16.0) + .color(colors::MID_GRAY), + ) + .selectable(false), + ) + }); + + let nip05_resp = name.nip05.map(|nip05| { + ui.image(egui::include_image!( + "../../../../../assets/icons/verified_4x.png" + )); + ui.add(Label::new( + RichText::new(nip05).size(16.0).color(colors::TEAL), + )) + }); + + (username_resp, nip05_resp) + }) + .inner; + + let resp = match (disp_resp, username_resp, nip05_resp) { + (Some(disp), Some(username), Some(nip05)) => disp.union(username).union(nip05), + (Some(disp), Some(username), None) => disp.union(username), + (Some(disp), None, None) => disp, + (None, Some(username), Some(nip05)) => username.union(nip05), + (None, Some(username), None) => username, + _ => ui.add(Label::new(RichText::new(name.name()))), }; if add_placeholder_space { @@ -185,7 +290,9 @@ where { move |ui: &mut egui::Ui| { if let Some(about) = profile.record().profile().and_then(|p| p.about()) { - ui.label(about) + let resp = ui.label(about); + ui.add_space(8.0); + resp } else { // need any Response so we dont need an Option ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()) From a1520fec7e6dcc87f26bc0aa4aa632eda6c74ae7 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 2 Jan 2025 14:57:23 -0500 Subject: [PATCH 06/16] edit profile button Signed-off-by: kernelkind --- assets/icons/edit_icon_4x_dark.png | Bin 0 -> 719 bytes crates/notedeck_columns/src/ui/profile/mod.rs | 94 +++++++++++++++--- 2 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 assets/icons/edit_icon_4x_dark.png diff --git a/assets/icons/edit_icon_4x_dark.png b/assets/icons/edit_icon_4x_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1da66c812b267dc3f09ac3072b58249bf3dd5b34 GIT binary patch literal 719 zcmV;=0x@~0drDELIAGL9O(c600d`2O+f$vv5yP_KR7wZ6Tr19YU0xeyQ<5BV9zJvK{%kGzD?c*+Y^ zR-#`L(v#QD@kv6O^4hs|Nk~^-tD{X4B!s35d9AJ{NsthlF6Fg4>)sAY%L}akI$T0% zvV4}$@>xF1XZb8Y%ks_Y%<@@2%V+s4-#7X5A>%V192Y7Y z))kjLgXK_E6O)GZ#3#?7{ptVGppIzdP2&TErECEnVA)J2j(V6|`k7in4=^RbHj=V{ z8e)?-g5?-|6DKw2#o6YU!E&;JiDSn((aLWc_6b{Ik~p9R2RDX+IFFjzl{mwNKq_%bo^?oq!)vjjhV>|k012-*K_rb40Zqj002ovPDHLkV1oMz BN7MiS literal 0 HcmV?d00001 diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index 51070f02..0f2b6a75 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -6,7 +6,7 @@ use crate::ui::note::NoteOptions; use crate::{colors, images}; use crate::{notes_holder::NotesHolder, NostrName}; use egui::load::TexturePoll; -use egui::{Label, RichText, Rounding, ScrollArea, Sense, Stroke}; +use egui::{vec2, Color32, Label, Layout, Rect, RichText, Rounding, ScrollArea, Sense, Stroke}; use enostr::Pubkey; use nostrdb::{Ndb, ProfileRecord, Transaction}; pub use picture::ProfilePic; @@ -108,21 +108,27 @@ impl<'a> ProfileView<'a> { pfp_rect.set_height(size); let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0)))); - ui.put( - pfp_rect, - ProfilePic::new(self.img_cache, get_profile_url(Some(&profile))).size(size), - ); + ui.horizontal(|ui| { + ui.put( + pfp_rect, + ProfilePic::new(self.img_cache, get_profile_url(Some(&profile))).size(size), + ); - if ui.add(copy_key_widget(&pfp_rect)).clicked() { - ui.output_mut(|w| { - w.copied_text = if let Some(bech) = self.pubkey.to_bech() { - bech - } else { - error!("Could not convert Pubkey to bech"); - String::new() - } + if ui.add(copy_key_widget(&pfp_rect)).clicked() { + ui.output_mut(|w| { + w.copied_text = if let Some(bech) = self.pubkey.to_bech() { + bech + } else { + error!("Could not convert Pubkey to bech"); + String::new() + } + }); + } + + ui.with_layout(Layout::right_to_left(egui::Align::Max), |ui| { + ui.add(edit_profile_button()) }); - } + }); ui.add_space(18.0); @@ -222,6 +228,66 @@ fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ { } } +fn edit_profile_button() -> impl egui::Widget + 'static { + |ui: &mut egui::Ui| -> egui::Response { + let (rect, resp) = ui.allocate_exact_size(vec2(124.0, 32.0), Sense::click()); + let painter = ui.painter_at(rect); + let rect = painter.round_rect_to_pixels(rect); + + painter.rect_filled( + rect, + Rounding::same(8.0), + if resp.hovered() { + ui.visuals().widgets.active.bg_fill + } else { + ui.visuals().widgets.inactive.bg_fill + }, + ); + painter.rect_stroke( + rect.shrink(1.0), + Rounding::same(8.0), + if resp.hovered() { + ui.visuals().widgets.active.bg_stroke + } else { + ui.visuals().widgets.inactive.bg_stroke + }, + ); + + let edit_icon_size = vec2(16.0, 16.0); + let galley = painter.layout( + "Edit Profile".to_owned(), + NotedeckTextStyle::Button.get_font_id(ui.ctx()), + ui.visuals().text_color(), + rect.width(), + ); + + let space_between_icon_galley = 8.0; + let half_icon_size = edit_icon_size.x / 2.0; + let galley_rect = { + let galley_rect = Rect::from_center_size(rect.center(), galley.rect.size()); + galley_rect.translate(vec2(half_icon_size + space_between_icon_galley / 2.0, 0.0)) + }; + + let edit_icon_rect = { + let mut center = galley_rect.left_center(); + center.x -= half_icon_size + space_between_icon_galley; + painter.round_rect_to_pixels(Rect::from_center_size( + painter.round_pos_to_pixel_center(center), + edit_icon_size, + )) + }; + + painter.galley(galley_rect.left_top(), galley, Color32::WHITE); + + egui::Image::new(egui::include_image!( + "../../../../../assets/icons/edit_icon_4x_dark.png" + )) + .paint_at(ui, edit_icon_rect); + + resp + } +} + fn display_name_widget(name: NostrName<'_>, add_placeholder_space: bool) -> impl egui::Widget + '_ { move |ui: &mut egui::Ui| -> egui::Response { let disp_resp = name.display_name.map(|disp_name| { From a1236692e5339522e780636ac5bfee02553bfca1 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:21:32 -0500 Subject: [PATCH 07/16] profile edit UI Signed-off-by: kernelkind --- crates/notedeck_columns/src/profile_state.rs | 79 +++++++ .../notedeck_columns/src/ui/profile/edit.rs | 205 ++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 crates/notedeck_columns/src/profile_state.rs create mode 100644 crates/notedeck_columns/src/ui/profile/edit.rs diff --git a/crates/notedeck_columns/src/profile_state.rs b/crates/notedeck_columns/src/profile_state.rs new file mode 100644 index 00000000..b08c3050 --- /dev/null +++ b/crates/notedeck_columns/src/profile_state.rs @@ -0,0 +1,79 @@ +use nostrdb::{NdbProfile, ProfileRecord}; + +#[derive(Default, Debug)] +pub struct ProfileState { + pub display_name: String, + pub name: String, + pub picture: String, + pub banner: String, + pub about: String, + pub website: String, + pub lud16: String, + pub nip05: String, +} + +impl ProfileState { + pub fn from_profile(record: &ProfileRecord<'_>) -> Self { + let display_name = get_item(record, |p| p.display_name()); + let username = get_item(record, |p| p.name()); + let profile_picture = get_item(record, |p| p.picture()); + let cover_image = get_item(record, |p| p.banner()); + let about = get_item(record, |p| p.about()); + let website = get_item(record, |p| p.website()); + let lud16 = get_item(record, |p| p.lud16()); + let nip05 = get_item(record, |p| p.nip05()); + + Self { + display_name, + name: username, + picture: profile_picture, + banner: cover_image, + about, + website, + lud16, + nip05, + } + } + + pub fn to_json(&self) -> String { + let mut fields = Vec::new(); + + if !self.display_name.is_empty() { + fields.push(format!(r#""display_name":"{}""#, self.display_name)); + } + if !self.name.is_empty() { + fields.push(format!(r#""name":"{}""#, self.name)); + } + if !self.picture.is_empty() { + fields.push(format!(r#""picture":"{}""#, self.picture)); + } + if !self.banner.is_empty() { + fields.push(format!(r#""banner":"{}""#, self.banner)); + } + if !self.about.is_empty() { + fields.push(format!(r#""about":"{}""#, self.about)); + } + if !self.website.is_empty() { + fields.push(format!(r#""website":"{}""#, self.website)); + } + if !self.lud16.is_empty() { + fields.push(format!(r#""lud16":"{}""#, self.lud16)); + } + if !self.nip05.is_empty() { + fields.push(format!(r#""nip05":"{}""#, self.nip05)); + } + + format!("{{{}}}", fields.join(",")) + } +} + +fn get_item<'a>( + record: &ProfileRecord<'a>, + item_retriever: fn(NdbProfile<'a>) -> Option<&'a str>, +) -> String { + record + .record() + .profile() + .and_then(item_retriever) + .map_or_else(String::new, ToString::to_string) +} diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs new file mode 100644 index 00000000..08ed8f00 --- /dev/null +++ b/crates/notedeck_columns/src/ui/profile/edit.rs @@ -0,0 +1,205 @@ +use core::f32; + +use egui::{vec2, Button, Layout, Margin, RichText, Rounding, ScrollArea, TextEdit}; +use notedeck::{ImageCache, NotedeckTextStyle}; + +use crate::{colors, profile_state::ProfileState}; + +use super::{banner, unwrap_profile_url, ProfilePic}; + +pub struct EditProfileView<'a> { + state: &'a mut ProfileState, + img_cache: &'a mut ImageCache, +} + +impl<'a> EditProfileView<'a> { + pub fn new(state: &'a mut ProfileState, img_cache: &'a mut ImageCache) -> Self { + Self { state, img_cache } + } + + // return true to save + pub fn ui(&mut self, ui: &mut egui::Ui) -> bool { + ScrollArea::vertical() + .show(ui, |ui| { + banner(ui, Some(&self.state.banner), 188.0); + + let padding = 24.0; + crate::ui::padding(padding, ui, |ui| { + self.inner(ui, padding); + }); + + ui.separator(); + + let mut save = false; + crate::ui::padding(padding, ui, |ui| { + ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| { + if ui + .add(button("Save changes", 119.0).fill(colors::PINK)) + .clicked() + { + save = true; + } + }); + }); + + save + }) + .inner + } + + fn inner(&mut self, ui: &mut egui::Ui, padding: f32) { + ui.spacing_mut().item_spacing = egui::vec2(0.0, 16.0); + let mut pfp_rect = ui.available_rect_before_wrap(); + let size = 80.0; + pfp_rect.set_width(size); + pfp_rect.set_height(size); + let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0)))); + + let pfp_url = unwrap_profile_url(if self.state.picture.is_empty() { + None + } else { + Some(&self.state.picture) + }); + ui.put( + pfp_rect, + ProfilePic::new(self.img_cache, pfp_url).size(size), + ); + + in_frame(ui, |ui| { + ui.add(label("Display name")); + ui.add(singleline_textedit(&mut self.state.display_name)); + }); + + in_frame(ui, |ui| { + ui.add(label("Username")); + ui.add(singleline_textedit(&mut self.state.name)); + }); + + in_frame(ui, |ui| { + ui.add(label("Profile picture")); + ui.add(multiline_textedit(&mut self.state.picture)); + }); + + in_frame(ui, |ui| { + ui.add(label("Banner")); + ui.add(multiline_textedit(&mut self.state.banner)); + }); + + in_frame(ui, |ui| { + ui.add(label("About")); + ui.add(multiline_textedit(&mut self.state.about)); + }); + + in_frame(ui, |ui| { + ui.add(label("Website")); + ui.add(singleline_textedit(&mut self.state.website)); + }); + + in_frame(ui, |ui| { + ui.add(label("Lightning network address (lud16)")); + ui.add(multiline_textedit(&mut self.state.lud16)); + }); + + in_frame(ui, |ui| { + ui.add(label("NIP-05 verification")); + ui.add(singleline_textedit(&mut self.state.nip05)); + let split = &mut self.state.nip05.split('@'); + let prefix = split.next(); + let suffix = split.next(); + if let Some(prefix) = prefix { + if let Some(suffix) = suffix { + let use_domain = if let Some(f) = prefix.chars().next() { + f == '_' + } else { + false + }; + ui.colored_label( + ui.visuals().noninteractive().fg_stroke.color, + RichText::new(if use_domain { + format!("\"{}\" will be used for verification", suffix) + } else { + format!( + "\"{}\" at \"{}\" will be used for verification", + prefix, suffix + ) + }), + ); + } + } + }); + } +} + +fn label(text: &str) -> impl egui::Widget + '_ { + move |ui: &mut egui::Ui| -> egui::Response { + ui.label(RichText::new(text).font(NotedeckTextStyle::Body.get_bolded_font(ui.ctx()))) + } +} + +fn singleline_textedit(data: &mut String) -> impl egui::Widget + '_ { + TextEdit::singleline(data) + .min_size(vec2(0.0, 40.0)) + .vertical_align(egui::Align::Center) + .margin(Margin::symmetric(12.0, 10.0)) + .desired_width(f32::INFINITY) +} + +fn multiline_textedit(data: &mut String) -> impl egui::Widget + '_ { + TextEdit::multiline(data) + // .min_size(vec2(0.0, 40.0)) + .vertical_align(egui::Align::TOP) + .margin(Margin::symmetric(12.0, 10.0)) + .desired_width(f32::INFINITY) + .desired_rows(1) +} + +fn in_frame(ui: &mut egui::Ui, contents: impl FnOnce(&mut egui::Ui)) { + egui::Frame::none().show(ui, |ui| { + ui.spacing_mut().item_spacing = egui::vec2(0.0, 8.0); + contents(ui); + }); +} + +fn button(text: &str, width: f32) -> egui::Button<'static> { + Button::new(text) + .rounding(Rounding::same(8.0)) + .min_size(vec2(width, 40.0)) +} + +mod preview { + use notedeck::App; + + use crate::{ + profile_state::ProfileState, + test_data, + ui::{Preview, PreviewConfig}, + }; + + use super::EditProfileView; + + pub struct EditProfilePreivew { + state: ProfileState, + } + + impl Default for EditProfilePreivew { + fn default() -> Self { + Self { + state: ProfileState::from_profile(&test_data::test_profile_record()), + } + } + } + + impl App for EditProfilePreivew { + fn update(&mut self, ctx: &mut notedeck::AppContext<'_>, ui: &mut egui::Ui) { + EditProfileView::new(&mut self.state, ctx.img_cache).ui(ui); + } + } + + impl<'a> Preview for EditProfileView<'a> { + type Prev = EditProfilePreivew; + + fn preview(_cfg: PreviewConfig) -> Self::Prev { + EditProfilePreivew::default() + } + } +} From d6f81991abf1bc3eeacbabfa94eadcb571f73290 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:30:56 -0500 Subject: [PATCH 08/16] refactor banner Signed-off-by: kernelkind --- crates/notedeck_columns/src/ui/profile/mod.rs | 51 ++++++++++--------- .../src/ui/profile/preview.rs | 8 +-- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index 0f2b6a75..beaedf3d 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -96,9 +96,11 @@ impl<'a> ProfileView<'a> { fn profile_body(&mut self, ui: &mut egui::Ui, profile: ProfileRecord<'_>) { ui.vertical(|ui| { - ui.add_sized([ui.available_size().x, 120.0], |ui: &mut egui::Ui| { - banner(ui, &profile) - }); + banner( + ui, + profile.record().profile().and_then(|p| p.banner()), + 120.0, + ); let padding = 12.0; crate::ui::padding(padding, ui, |ui| { @@ -343,7 +345,11 @@ fn display_name_widget(name: NostrName<'_>, add_placeholder_space: bool) -> impl } pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { - if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { + unwrap_profile_url(profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture()))) +} + +pub fn unwrap_profile_url(maybe_url: Option<&str>) -> &str { + if let Some(url) = maybe_url { url } else { ProfilePic::no_pfp_url() @@ -366,16 +372,11 @@ where } } -fn banner_texture( - ui: &mut egui::Ui, - profile: &ProfileRecord<'_>, -) -> Option { +fn banner_texture(ui: &mut egui::Ui, banner_url: &str) -> Option { // TODO: cache banner - let banner = profile.record().profile().and_then(|p| p.banner()); - - if let Some(banner) = banner { + if !banner_url.is_empty() { let texture_load_res = - egui::Image::new(banner).load_for_size(ui.ctx(), ui.available_size()); + egui::Image::new(banner_url).load_for_size(ui.ctx(), ui.available_size()); if let Ok(texture_poll) = texture_load_res { match texture_poll { TexturePoll::Pending { .. } => {} @@ -387,16 +388,18 @@ fn banner_texture( None } -fn banner(ui: &mut egui::Ui, profile: &ProfileRecord<'_>) -> egui::Response { - if let Some(texture) = banner_texture(ui, profile) { - images::aspect_fill( - ui, - Sense::hover(), - texture.id, - texture.size.x / texture.size.y, - ) - } else { - // TODO: default banner texture - ui.label("") - } +fn banner(ui: &mut egui::Ui, banner_url: Option<&str>, height: f32) -> egui::Response { + ui.add_sized([ui.available_size().x, height], |ui: &mut egui::Ui| { + banner_url + .and_then(|url| banner_texture(ui, url)) + .map(|texture| { + images::aspect_fill( + ui, + Sense::hover(), + texture.id, + texture.size.x / texture.size.y, + ) + }) + .unwrap_or_else(|| ui.label("")) + }) } diff --git a/crates/notedeck_columns/src/ui/profile/preview.rs b/crates/notedeck_columns/src/ui/profile/preview.rs index 0befac3f..c52655d5 100644 --- a/crates/notedeck_columns/src/ui/profile/preview.rs +++ b/crates/notedeck_columns/src/ui/profile/preview.rs @@ -53,9 +53,11 @@ impl<'a, 'cache> ProfilePreview<'a, 'cache> { impl egui::Widget for ProfilePreview<'_, '_> { fn ui(self, ui: &mut egui::Ui) -> egui::Response { ui.vertical(|ui| { - ui.add_sized([ui.available_size().x, 80.0], |ui: &mut egui::Ui| { - banner(ui, self.profile) - }); + banner( + ui, + self.profile.record().profile().and_then(|p| p.banner()), + 80.0, + ); self.body(ui); }) From eac24ac982784745d127d1899df30999b0035338 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:31:17 -0500 Subject: [PATCH 09/16] get bolded font helper Signed-off-by: kernelkind --- crates/notedeck/src/style.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/notedeck/src/style.rs b/crates/notedeck/src/style.rs index 2a890457..d54ff572 100644 --- a/crates/notedeck/src/style.rs +++ b/crates/notedeck/src/style.rs @@ -49,4 +49,11 @@ impl NotedeckTextStyle { pub fn get_font_id(&self, ctx: &Context) -> FontId { FontId::new(get_font_size(ctx, self), self.font_family()) } + + pub fn get_bolded_font(&self, ctx: &Context) -> FontId { + FontId::new( + get_font_size(ctx, self), + egui::FontFamily::Name(crate::NamedFontFamily::Bold.as_str().into()), + ) + } } From 4baa7b2ef33884d4be2fbcb51a3413b7bdad9ed4 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:22:11 -0500 Subject: [PATCH 10/16] use preview Signed-off-by: kernelkind --- crates/notedeck_chrome/src/preview.rs | 2 ++ crates/notedeck_columns/src/lib.rs | 1 + crates/notedeck_columns/src/ui/profile/mod.rs | 2 ++ 3 files changed, 5 insertions(+) diff --git a/crates/notedeck_chrome/src/preview.rs b/crates/notedeck_chrome/src/preview.rs index 9d818cb7..96cc5db1 100644 --- a/crates/notedeck_chrome/src/preview.rs +++ b/crates/notedeck_chrome/src/preview.rs @@ -3,6 +3,7 @@ use notedeck_chrome::setup::generate_native_options; use notedeck_chrome::Notedeck; use notedeck_columns::ui::configure_deck::ConfigureDeckView; use notedeck_columns::ui::edit_deck::EditDeckView; +use notedeck_columns::ui::profile::EditProfileView; use notedeck_columns::ui::{ account_login_view::AccountLoginView, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview, RelayView, @@ -95,5 +96,6 @@ async fn main() { PostView, ConfigureDeckView, EditDeckView, + EditProfileView, ); } diff --git a/crates/notedeck_columns/src/lib.rs b/crates/notedeck_columns/src/lib.rs index 06b1309f..5fed2e25 100644 --- a/crates/notedeck_columns/src/lib.rs +++ b/crates/notedeck_columns/src/lib.rs @@ -23,6 +23,7 @@ mod nav; mod notes_holder; mod post; mod profile; +mod profile_state; pub mod relay_pool_manager; mod route; mod subscriptions; diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index beaedf3d..bb52f732 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -1,3 +1,4 @@ +pub mod edit; pub mod picture; pub mod preview; @@ -5,6 +6,7 @@ use crate::profile::get_display_name; use crate::ui::note::NoteOptions; use crate::{colors, images}; use crate::{notes_holder::NotesHolder, NostrName}; +pub use edit::EditProfileView; use egui::load::TexturePoll; use egui::{vec2, Color32, Label, Layout, Rect, RichText, Rounding, ScrollArea, Sense, Stroke}; use enostr::Pubkey; From b1a84788ff2e1a3fe254a749558f2ca052fa5f43 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:34:11 -0500 Subject: [PATCH 11/16] move show_profile to its own fn Signed-off-by: kernelkind --- .../notedeck_columns/src/ui/column/header.rs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs index e47ad648..69b0bda4 100644 --- a/crates/notedeck_columns/src/ui/column/header.rs +++ b/crates/notedeck_columns/src/ui/column/header.rs @@ -245,15 +245,7 @@ impl<'a> NavTitle<'a> { TimelineRoute::Quote(_note_id) => {} TimelineRoute::Profile(pubkey) => { - let txn = Transaction::new(self.ndb).unwrap(); - if let Some(pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) { - ui.add(pfp); - } else { - ui.add( - ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()) - .size(pfp_size), - ); - } + self.show_profile(ui, pubkey, pfp_size); } }, @@ -267,6 +259,17 @@ impl<'a> NavTitle<'a> { } } + fn show_profile(&mut self, ui: &mut egui::Ui, pubkey: &Pubkey, pfp_size: f32) { + let txn = Transaction::new(self.ndb).unwrap(); + if let Some(pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) { + ui.add(pfp); + } else { + ui.add( + ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size), + ); + }; + } + fn title_label_value(title: &str) -> egui::Label { egui::Label::new(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style())) .selectable(false) From 5fd9f32ba7778ce7e0d531d0e8ccbfe78b43f51a Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:38:47 -0500 Subject: [PATCH 12/16] prelim fns for edit profiles Signed-off-by: kernelkind --- crates/notedeck/src/accounts.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/notedeck/src/accounts.rs b/crates/notedeck/src/accounts.rs index 354be843..21a8cd41 100644 --- a/crates/notedeck/src/accounts.rs +++ b/crates/notedeck/src/accounts.rs @@ -330,6 +330,14 @@ impl Accounts { None } + pub fn contains_full_kp(&self, pubkey: &enostr::Pubkey) -> bool { + if let Some(contains) = self.contains_account(pubkey.bytes()) { + contains.has_nsec + } else { + false + } + } + #[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"] pub fn add_account(&mut self, account: Keypair) -> AddAccountAction { let pubkey = account.pubkey; @@ -562,6 +570,18 @@ impl Accounts { self.needs_relay_config = false; } } + + pub fn get_full<'a>(&'a self, pubkey: &[u8; 32]) -> Option> { + if let Some(contains) = self.contains_account(pubkey) { + if contains.has_nsec { + if let Some(kp) = self.get_account(contains.index) { + return kp.to_full(); + } + } + } + + None + } } fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option { From 4ea17f8920c6575a57b2b5b89f6a648813be150f Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:40:19 -0500 Subject: [PATCH 13/16] holder for ProfileState Signed-off-by: kernelkind --- crates/notedeck_columns/src/view_state.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/notedeck_columns/src/view_state.rs b/crates/notedeck_columns/src/view_state.rs index c0a20b2c..0483a036 100644 --- a/crates/notedeck_columns/src/view_state.rs +++ b/crates/notedeck_columns/src/view_state.rs @@ -1,7 +1,10 @@ use std::collections::HashMap; +use enostr::Pubkey; + use crate::deck_state::DeckState; use crate::login_manager::AcquireKeyState; +use crate::profile_state::ProfileState; /// Various state for views #[derive(Default)] @@ -10,6 +13,7 @@ pub struct ViewState { pub id_to_deck_state: HashMap, pub id_state_map: HashMap, pub id_string_map: HashMap, + pub pubkey_to_profile_state: HashMap, } impl ViewState { From 3384d2af148751430a51756ce9658e0a4636a6d0 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:44:47 -0500 Subject: [PATCH 14/16] new profile responses Signed-off-by: kernelkind --- crates/notedeck_columns/src/profile.rs | 37 ++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/notedeck_columns/src/profile.rs b/crates/notedeck_columns/src/profile.rs index 229ab957..abcb7d3b 100644 --- a/crates/notedeck_columns/src/profile.rs +++ b/crates/notedeck_columns/src/profile.rs @@ -1,11 +1,12 @@ -use enostr::{Filter, Pubkey}; -use nostrdb::{FilterBuilder, Ndb, ProfileRecord, Transaction}; +use enostr::{Filter, FullKeypair, Pubkey}; +use nostrdb::{FilterBuilder, Ndb, Note, NoteBuilder, ProfileRecord, Transaction}; use notedeck::{filter::default_limit, FilterState, MuteFun, NoteCache, NoteRef}; use crate::{ multi_subscriber::MultiSubscriber, notes_holder::NotesHolder, + profile_state::ProfileState, timeline::{copy_notes_into_timeline, PubkeySource, Timeline, TimelineKind, TimelineTab}, }; @@ -155,3 +156,35 @@ impl NotesHolder for Profile { self.multi_subscriber = Some(subscriber); } } + +pub struct SaveProfileChanges { + pub kp: FullKeypair, + pub state: ProfileState, +} + +impl SaveProfileChanges { + pub fn new(kp: FullKeypair, state: ProfileState) -> Self { + Self { kp, state } + } + pub fn to_note(&self) -> Note { + let sec = &self.kp.secret_key.to_secret_bytes(); + add_client_tag(NoteBuilder::new()) + .kind(0) + .content(&self.state.to_json()) + .sign(sec) + .build() + .expect("should build") + } +} + +fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> { + builder + .start_tag() + .tag_str("client") + .tag_str("Damus Notedeck") +} + +pub enum ProfileAction { + Edit(FullKeypair), + SaveChanges(SaveProfileChanges), +} From 6645d4880f912e5d925a7ad43fd8642c8505db39 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:50:48 -0500 Subject: [PATCH 15/16] integrate EditProfileView Signed-off-by: kernelkind --- crates/notedeck_columns/src/nav.rs | 43 ++++++++++++++++++- crates/notedeck_columns/src/profile.rs | 30 +++++++++++-- crates/notedeck_columns/src/route.rs | 3 ++ crates/notedeck_columns/src/storage/decks.rs | 14 ++++++ crates/notedeck_columns/src/timeline/route.rs | 20 +++++++-- .../notedeck_columns/src/ui/column/header.rs | 3 ++ crates/notedeck_columns/src/ui/profile/mod.rs | 39 +++++++++++++---- 7 files changed, 137 insertions(+), 15 deletions(-) diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index a9746c7c..d96702ef 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -6,7 +6,8 @@ use crate::{ deck_state::DeckState, decks::{Deck, DecksAction, DecksCache}, notes_holder::NotesHolder, - profile::Profile, + profile::{Profile, ProfileAction, SaveProfileChanges}, + profile_state::ProfileState, relay_pool_manager::RelayPoolManager, route::Route, thread::Thread, @@ -21,6 +22,7 @@ use crate::{ configure_deck::ConfigureDeckView, edit_deck::{EditDeckResponse, EditDeckView}, note::{PostAction, PostType}, + profile::EditProfileView, support::SupportView, RelayView, View, }, @@ -39,6 +41,7 @@ pub enum RenderNavAction { RemoveColumn, PostAction(PostAction), NoteAction(NoteAction), + ProfileAction(ProfileAction), SwitchingAction(SwitchingAction), } @@ -168,6 +171,15 @@ impl RenderNavResponse { RenderNavAction::SwitchingAction(switching_action) => { switching_occured = switching_action.process(&mut app.decks_cache, ctx); } + RenderNavAction::ProfileAction(profile_action) => { + profile_action.process( + ctx.ndb, + ctx.pool, + get_active_columns_mut(ctx.accounts, &mut app.decks_cache) + .column_mut(col) + .router_mut(), + ); + } } } @@ -368,6 +380,35 @@ fn render_nav_body( action } + Route::EditProfile(pubkey) => { + let mut action = None; + if let Some(kp) = ctx.accounts.get_full(pubkey.bytes()) { + let state = app + .view_state + .pubkey_to_profile_state + .entry(*kp.pubkey) + .or_insert_with(|| { + let txn = Transaction::new(ctx.ndb).expect("txn"); + if let Ok(record) = ctx.ndb.get_profile_by_pubkey(&txn, kp.pubkey.bytes()) { + ProfileState::from_profile(&record) + } else { + ProfileState::default() + } + }); + if EditProfileView::new(state, ctx.img_cache).ui(ui) { + if let Some(taken_state) = + app.view_state.pubkey_to_profile_state.remove(kp.pubkey) + { + action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges( + SaveProfileChanges::new(kp.to_full(), taken_state), + ))) + } + } + } else { + error!("Pubkey in EditProfile route did not have an nsec attached in Accounts"); + } + action + } } } diff --git a/crates/notedeck_columns/src/profile.rs b/crates/notedeck_columns/src/profile.rs index abcb7d3b..03fbc0be 100644 --- a/crates/notedeck_columns/src/profile.rs +++ b/crates/notedeck_columns/src/profile.rs @@ -1,12 +1,16 @@ -use enostr::{Filter, FullKeypair, Pubkey}; -use nostrdb::{FilterBuilder, Ndb, Note, NoteBuilder, ProfileRecord, Transaction}; +use enostr::{Filter, FullKeypair, Pubkey, RelayPool}; +use nostrdb::{ + FilterBuilder, Ndb, Note, NoteBuildOptions, NoteBuilder, ProfileRecord, Transaction, +}; use notedeck::{filter::default_limit, FilterState, MuteFun, NoteCache, NoteRef}; +use tracing::info; use crate::{ multi_subscriber::MultiSubscriber, notes_holder::NotesHolder, profile_state::ProfileState, + route::{Route, Router}, timeline::{copy_notes_into_timeline, PubkeySource, Timeline, TimelineKind, TimelineTab}, }; @@ -171,7 +175,7 @@ impl SaveProfileChanges { add_client_tag(NoteBuilder::new()) .kind(0) .content(&self.state.to_json()) - .sign(sec) + .options(NoteBuildOptions::default().created_at(true).sign(sec)) .build() .expect("should build") } @@ -188,3 +192,23 @@ pub enum ProfileAction { Edit(FullKeypair), SaveChanges(SaveProfileChanges), } + +impl ProfileAction { + pub fn process(&self, ndb: &Ndb, pool: &mut RelayPool, router: &mut Router) { + match self { + ProfileAction::Edit(kp) => { + router.route_to(Route::EditProfile(kp.pubkey)); + } + ProfileAction::SaveChanges(changes) => { + let raw_msg = format!("[\"EVENT\",{}]", changes.to_note().json().unwrap()); + + let _ = ndb.process_client_event(raw_msg.as_str()); + + info!("sending {}", raw_msg); + pool.send(&enostr::ClientMessage::raw(raw_msg)); + + router.go_back(); + } + } + } +} diff --git a/crates/notedeck_columns/src/route.rs b/crates/notedeck_columns/src/route.rs index 2fad3c27..647aa66b 100644 --- a/crates/notedeck_columns/src/route.rs +++ b/crates/notedeck_columns/src/route.rs @@ -16,6 +16,7 @@ pub enum Route { Relays, ComposeNote, AddColumn(AddColumnRoute), + EditProfile(Pubkey), Support, NewDeck, EditDeck(usize), @@ -104,6 +105,7 @@ impl Route { Route::Support => ColumnTitle::simple("Damus Support"), Route::NewDeck => ColumnTitle::simple("Add Deck"), Route::EditDeck(_) => ColumnTitle::simple("Edit Deck"), + Route::EditProfile(_) => ColumnTitle::simple("Edit Profile"), } } } @@ -215,6 +217,7 @@ impl fmt::Display for Route { Route::Support => write!(f, "Support"), Route::NewDeck => write!(f, "Add Deck"), Route::EditDeck(_) => write!(f, "Edit Deck"), + Route::EditProfile(_) => write!(f, "Edit Profile"), } } } diff --git a/crates/notedeck_columns/src/storage/decks.rs b/crates/notedeck_columns/src/storage/decks.rs index b928692b..5bd79e5b 100644 --- a/crates/notedeck_columns/src/storage/decks.rs +++ b/crates/notedeck_columns/src/storage/decks.rs @@ -541,6 +541,11 @@ fn serialize_route(route: &Route, columns: &Columns) -> Option { selections.push(Selection::Keyword(Keyword::Edit)); selections.push(Selection::Payload(index.to_string())); } + Route::EditProfile(pubkey) => { + selections.push(Selection::Keyword(Keyword::Profile)); + selections.push(Selection::Keyword(Keyword::Edit)); + selections.push(Selection::Payload(pubkey.hex())); + } } if selections.is_empty() { @@ -649,6 +654,15 @@ fn selections_to_route(selections: Vec) -> Option Some(CleanIntermediaryRoute::ToTimeline( TimelineKind::profile(PubkeySource::DeckAuthor), )), + Selection::Keyword(Keyword::Edit) => { + if let Selection::Payload(hex) = selections.get(2)? { + Some(CleanIntermediaryRoute::ToRoute(Route::EditProfile( + Pubkey::from_hex(hex.as_str()).ok()?, + ))) + } else { + None + } + } _ => None, }, Selection::Keyword(Keyword::Universe) => { diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs index 50485892..851d19d5 100644 --- a/crates/notedeck_columns/src/timeline/route.rs +++ b/crates/notedeck_columns/src/timeline/route.rs @@ -3,7 +3,7 @@ use crate::{ draft::Drafts, nav::RenderNavAction, notes_holder::NotesHolderStorage, - profile::Profile, + profile::{Profile, ProfileAction}, thread::Thread, timeline::{TimelineId, TimelineKind}, ui::{ @@ -117,6 +117,7 @@ pub fn render_timeline_route( TimelineRoute::Profile(pubkey) => render_profile_route( &pubkey, + accounts, ndb, profiles, img_cache, @@ -155,6 +156,7 @@ pub fn render_timeline_route( #[allow(clippy::too_many_arguments)] pub fn render_profile_route( pubkey: &Pubkey, + accounts: &Accounts, ndb: &Ndb, profiles: &mut NotesHolderStorage, img_cache: &mut ImageCache, @@ -163,8 +165,9 @@ pub fn render_profile_route( ui: &mut egui::Ui, is_muted: &MuteFun, ) -> Option { - let note_action = ProfileView::new( + let action = ProfileView::new( pubkey, + accounts, col, profiles, ndb, @@ -174,5 +177,16 @@ pub fn render_profile_route( ) .ui(ui, is_muted); - note_action.map(RenderNavAction::NoteAction) + if let Some(action) = action { + match action { + ui::profile::ProfileViewAction::EditProfile => accounts + .get_full(pubkey.bytes()) + .map(|kp| RenderNavAction::ProfileAction(ProfileAction::Edit(kp.to_full()))), + ui::profile::ProfileViewAction::Note(note_action) => { + Some(RenderNavAction::NoteAction(note_action)) + } + } + } else { + None + } } diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs index 69b0bda4..8b61419f 100644 --- a/crates/notedeck_columns/src/ui/column/header.rs +++ b/crates/notedeck_columns/src/ui/column/header.rs @@ -256,6 +256,9 @@ impl<'a> NavTitle<'a> { Route::Relays => {} Route::NewDeck => {} Route::EditDeck(_) => {} + Route::EditProfile(pubkey) => { + self.show_profile(ui, pubkey, pfp_size); + } } } diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index bb52f732..001f66ed 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -18,10 +18,11 @@ use tracing::error; use crate::{actionbar::NoteAction, notes_holder::NotesHolderStorage, profile::Profile}; use super::timeline::{tabs_ui, TimelineTabView}; -use notedeck::{ImageCache, MuteFun, NoteCache, NotedeckTextStyle}; +use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, NotedeckTextStyle}; pub struct ProfileView<'a> { pubkey: &'a Pubkey, + accounts: &'a Accounts, col_id: usize, profiles: &'a mut NotesHolderStorage, note_options: NoteOptions, @@ -30,9 +31,16 @@ pub struct ProfileView<'a> { img_cache: &'a mut ImageCache, } +pub enum ProfileViewAction { + EditProfile, + Note(NoteAction), +} + impl<'a> ProfileView<'a> { + #[allow(clippy::too_many_arguments)] pub fn new( pubkey: &'a Pubkey, + accounts: &'a Accounts, col_id: usize, profiles: &'a mut NotesHolderStorage, ndb: &'a Ndb, @@ -42,6 +50,7 @@ impl<'a> ProfileView<'a> { ) -> Self { ProfileView { pubkey, + accounts, col_id, profiles, ndb, @@ -51,15 +60,18 @@ impl<'a> ProfileView<'a> { } } - pub fn ui(&mut self, ui: &mut egui::Ui, is_muted: &MuteFun) -> Option { + pub fn ui(&mut self, ui: &mut egui::Ui, is_muted: &MuteFun) -> Option { let scroll_id = egui::Id::new(("profile_scroll", self.col_id, self.pubkey)); ScrollArea::vertical() .id_salt(scroll_id) .show(ui, |ui| { + let mut action = None; let txn = Transaction::new(self.ndb).expect("txn"); if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, self.pubkey.bytes()) { - self.profile_body(ui, profile); + if self.profile_body(ui, profile) { + action = Some(ProfileViewAction::EditProfile); + } } let profile = self .profiles @@ -82,7 +94,7 @@ impl<'a> ProfileView<'a> { let reversed = false; - TimelineTabView::new( + if let Some(note_action) = TimelineTabView::new( profile.timeline.current_view(), reversed, self.note_options, @@ -92,11 +104,16 @@ impl<'a> ProfileView<'a> { self.img_cache, ) .show(ui) + { + action = Some(ProfileViewAction::Note(note_action)); + } + action }) .inner } - fn profile_body(&mut self, ui: &mut egui::Ui, profile: ProfileRecord<'_>) { + fn profile_body(&mut self, ui: &mut egui::Ui, profile: ProfileRecord<'_>) -> bool { + let mut action = false; ui.vertical(|ui| { banner( ui, @@ -129,9 +146,13 @@ impl<'a> ProfileView<'a> { }); } - ui.with_layout(Layout::right_to_left(egui::Align::Max), |ui| { - ui.add(edit_profile_button()) - }); + if self.accounts.contains_full_kp(self.pubkey) { + ui.with_layout(Layout::right_to_left(egui::Align::Max), |ui| { + if ui.add(edit_profile_button()).clicked() { + action = true; + } + }); + } }); ui.add_space(18.0); @@ -163,6 +184,8 @@ impl<'a> ProfileView<'a> { }); }); }); + + action } } From 05ab1179e6fecf60e7328d930c621c23d9cf1363 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Sat, 4 Jan 2025 14:05:42 -0500 Subject: [PATCH 16/16] remove ProfileState from cache once sent Signed-off-by: kernelkind --- crates/notedeck_columns/src/nav.rs | 1 + crates/notedeck_columns/src/profile.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index d96702ef..4d76f22c 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -173,6 +173,7 @@ impl RenderNavResponse { } RenderNavAction::ProfileAction(profile_action) => { profile_action.process( + &mut app.view_state.pubkey_to_profile_state, ctx.ndb, ctx.pool, get_active_columns_mut(ctx.accounts, &mut app.decks_cache) diff --git a/crates/notedeck_columns/src/profile.rs b/crates/notedeck_columns/src/profile.rs index 03fbc0be..436cfc20 100644 --- a/crates/notedeck_columns/src/profile.rs +++ b/crates/notedeck_columns/src/profile.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use enostr::{Filter, FullKeypair, Pubkey, RelayPool}; use nostrdb::{ FilterBuilder, Ndb, Note, NoteBuildOptions, NoteBuilder, ProfileRecord, Transaction, @@ -194,7 +196,13 @@ pub enum ProfileAction { } impl ProfileAction { - pub fn process(&self, ndb: &Ndb, pool: &mut RelayPool, router: &mut Router) { + pub fn process( + &self, + state_map: &mut HashMap, + ndb: &Ndb, + pool: &mut RelayPool, + router: &mut Router, + ) { match self { ProfileAction::Edit(kp) => { router.route_to(Route::EditProfile(kp.pubkey)); @@ -203,6 +211,7 @@ impl ProfileAction { let raw_msg = format!("[\"EVENT\",{}]", changes.to_note().json().unwrap()); let _ = ndb.process_client_event(raw_msg.as_str()); + let _ = state_map.remove_entry(&changes.kp.pubkey); info!("sending {}", raw_msg); pool.send(&enostr::ClientMessage::raw(raw_msg));