From a1236692e5339522e780636ac5bfee02553bfca1 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 3 Jan 2025 17:21:32 -0500 Subject: [PATCH] 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() + } + } +}