commit a714bef690d8bf6906646579e23f2e1dd1914721
parent 4e3fcad7091d8857e98a32008ce628875c1c010e
Author: William Casarin <jb55@jb55.com>
Date: Tue, 15 Jul 2025 08:24:46 -0700
ui/profile: fix dubious profile editing
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
9 files changed, 148 insertions(+), 188 deletions(-)
diff --git a/crates/enostr/src/lib.rs b/crates/enostr/src/lib.rs
@@ -14,7 +14,7 @@ pub use filter::Filter;
pub use keypair::{FilledKeypair, FullKeypair, Keypair, KeypairUnowned, SerializableKeypair};
pub use nostr::SecretKey;
pub use note::{Note, NoteId};
-pub use profile::Profile;
+pub use profile::ProfileState;
pub use pubkey::{Pubkey, PubkeyRef};
pub use relay::message::{RelayEvent, RelayMessage};
pub use relay::pool::{PoolEvent, PoolRelay, RelayPool};
diff --git a/crates/enostr/src/profile.rs b/crates/enostr/src/profile.rs
@@ -1,38 +1,104 @@
-use serde_json::Value;
+use serde_json::{Map, Value};
-#[derive(Debug, Clone)]
-pub struct Profile(Value);
+#[derive(Debug, Clone, Default)]
+pub struct ProfileState(Value);
-impl Profile {
- pub fn new(value: Value) -> Profile {
- Profile(value)
+impl ProfileState {
+ pub fn new(value: Map<String, Value>) -> Self {
+ Self(Value::Object(value))
}
+ pub fn get_str(&self, name: &str) -> Option<&str> {
+ self.0.get(name).and_then(|v| v.as_str())
+ }
+
+ pub fn values_mut(&mut self) -> &mut Map<String, Value> {
+ self.0.as_object_mut().unwrap()
+ }
+
+ /// Insert or overwrite an existing value with a string
+ pub fn str_mut(&mut self, name: &str) -> &mut String {
+ let val = self
+ .values_mut()
+ .entry(name)
+ .or_insert(Value::String("".to_string()));
+
+ // if its not a string, make it one. this will overrwrite
+ // the old value, so be careful
+ if !val.is_string() {
+ *val = Value::String("".to_string());
+ }
+
+ match val {
+ Value::String(s) => s,
+ // SAFETY: we replace it above, so its impossible to be something
+ // other than a string
+ _ => panic!("impossible"),
+ }
+ }
+
+ pub fn value(&self) -> &Value {
+ &self.0
+ }
+
+ pub fn to_json(&self) -> String {
+ // SAFETY: serializing a value should be irrefutable
+ serde_json::to_string(self.value()).unwrap()
+ }
+
+ #[inline]
pub fn name(&self) -> Option<&str> {
- self.0["name"].as_str()
+ self.get_str("name")
}
+ #[inline]
+ pub fn banner(&self) -> Option<&str> {
+ self.get_str("name")
+ }
+
+ #[inline]
pub fn display_name(&self) -> Option<&str> {
- self.0["display_name"].as_str()
+ self.get_str("display_name")
}
+ #[inline]
pub fn lud06(&self) -> Option<&str> {
- self.0["lud06"].as_str()
+ self.get_str("lud06")
}
+ #[inline]
+ pub fn nip05(&self) -> Option<&str> {
+ self.get_str("nip05")
+ }
+
+ #[inline]
pub fn lud16(&self) -> Option<&str> {
- self.0["lud16"].as_str()
+ self.get_str("lud16")
}
+ #[inline]
pub fn about(&self) -> Option<&str> {
- self.0["about"].as_str()
+ self.get_str("about")
}
+ #[inline]
pub fn picture(&self) -> Option<&str> {
- self.0["picture"].as_str()
+ self.get_str("picture")
}
+ #[inline]
pub fn website(&self) -> Option<&str> {
- self.0["website"].as_str()
+ self.get_str("website")
+ }
+
+ pub fn from_note_contents(contents: &str) -> Self {
+ let json = serde_json::from_str(contents);
+ let data = if let Ok(Value::Object(data)) = json {
+ data
+ } else {
+ Map::new()
+ };
+
+ Self::new(data)
}
}
diff --git a/crates/notedeck_columns/src/lib.rs b/crates/notedeck_columns/src/lib.rs
@@ -19,7 +19,6 @@ mod multi_subscriber;
mod nav;
mod post;
mod profile;
-mod profile_state;
mod route;
mod search;
mod subscriptions;
diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs
@@ -5,7 +5,6 @@ use crate::{
deck_state::DeckState,
decks::{Deck, DecksAction, DecksCache},
profile::{ProfileAction, SaveProfileChanges},
- profile_state::ProfileState,
route::{Route, Router, SingletonRouter},
timeline::{
route::{render_thread_route, render_timeline_route},
@@ -28,9 +27,11 @@ use crate::{
};
use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, PopupSheet};
-use nostrdb::Transaction;
+use enostr::ProfileState;
+use nostrdb::{Filter, Transaction};
use notedeck::{
- get_current_default_msats, get_current_wallet, AppContext, NoteAction, NoteContext, RelayAction,
+ get_current_default_msats, get_current_wallet, ui::is_narrow, AppContext, NoteAction,
+ NoteContext, RelayAction,
};
use tracing::error;
@@ -674,8 +675,17 @@ fn render_nav_body(
.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)
+ let filter = Filter::new_with_capacity(1)
+ .kinds([0])
+ .authors([kp.pubkey.bytes()])
+ .build();
+
+ let Ok(results) = ctx.ndb.query(&txn, &[filter], 1) else {
+ return ProfileState::default();
+ };
+
+ if let Some(result) = results.first() {
+ ProfileState::from_note_contents(result.note.content())
} else {
ProfileState::default()
}
diff --git a/crates/notedeck_columns/src/profile.rs b/crates/notedeck_columns/src/profile.rs
@@ -1,12 +1,12 @@
use std::collections::HashMap;
-use enostr::{FilledKeypair, FullKeypair, Pubkey, RelayPool};
+use enostr::{FilledKeypair, FullKeypair, ProfileState, Pubkey, RelayPool};
use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder, Transaction};
use notedeck::{Accounts, ContactState};
use tracing::info;
-use crate::{nav::RouterAction, profile_state::ProfileState, route::Route};
+use crate::{nav::RouterAction, route::Route};
pub struct SaveProfileChanges {
pub kp: FullKeypair,
@@ -149,13 +149,17 @@ fn send_kind_3_event(ndb: &Ndb, pool: &mut RelayPool, accounts: &Accounts, actio
let contact_note = match ndb.get_note_by_key(&txn, *note_key).ok() {
Some(n) => n,
None => {
- tracing::error!("Somehow we are in state ContactState::Received but the contact note key doesn't exist");
+ tracing::error!(
+ "Somehow we are in state ContactState::Received but the contact note key doesn't exist"
+ );
return;
}
};
if contact_note.kind() != 3 {
- tracing::error!("Something very wrong just occured. The key for the supposed contact note yielded a note which was not a contact...");
+ tracing::error!(
+ "Something very wrong just occured. The key for the supposed contact note yielded a note which was not a contact..."
+ );
return;
}
diff --git a/crates/notedeck_columns/src/profile_state.rs b/crates/notedeck_columns/src/profile_state.rs
@@ -1,79 +0,0 @@
-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/test_data.rs b/crates/notedeck_columns/src/test_data.rs
@@ -20,7 +20,7 @@ pub fn sample_pool() -> RelayPool {
}
// my (jb55) profile
-const TEST_PROFILE_DATA: [u8; 448] = [
+const _TEST_PROFILE_DATA: [u8; 448] = [
0x04, 0x00, 0x00, 0x00, 0x54, 0xfe, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xd6, 0xd9, 0xc6, 0x65, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x66, 0x69, 0x78, 0x6d,
@@ -62,8 +62,8 @@ pub fn test_pubkey() -> &'static [u8; 32] {
}
*/
-pub fn test_profile_record() -> ProfileRecord<'static> {
- ProfileRecord::new_owned(&TEST_PROFILE_DATA).unwrap()
+pub fn _test_profile_record() -> ProfileRecord<'static> {
+ ProfileRecord::new_owned(&_TEST_PROFILE_DATA).unwrap()
}
/*
diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs
@@ -1,7 +1,7 @@
use core::f32;
-use crate::profile_state::ProfileState;
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
+use enostr::ProfileState;
use notedeck::{profile::unwrap_profile_url, Images, NotedeckTextStyle};
use notedeck_ui::{profile::banner, ProfilePic};
@@ -19,7 +19,7 @@ impl<'a> EditProfileView<'a> {
pub fn ui(&mut self, ui: &mut egui::Ui) -> bool {
ScrollArea::vertical()
.show(ui, |ui| {
- banner(ui, Some(&self.state.banner), 188.0);
+ banner(ui, self.state.banner(), 188.0);
let padding = 24.0;
notedeck_ui::padding(padding, ui, |ui| {
@@ -53,11 +53,7 @@ impl<'a> EditProfileView<'a> {
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)
- });
+ let pfp_url = unwrap_profile_url(self.state.picture());
ui.put(
pfp_rect,
&mut ProfilePic::new(self.img_cache, pfp_url)
@@ -67,65 +63,72 @@ impl<'a> EditProfileView<'a> {
in_frame(ui, |ui| {
ui.add(label("Display name"));
- ui.add(singleline_textedit(&mut self.state.display_name));
+ ui.add(singleline_textedit(self.state.str_mut("display_name")));
});
in_frame(ui, |ui| {
ui.add(label("Username"));
- ui.add(singleline_textedit(&mut self.state.name));
+ ui.add(singleline_textedit(self.state.str_mut("name")));
});
in_frame(ui, |ui| {
ui.add(label("Profile picture"));
- ui.add(multiline_textedit(&mut self.state.picture));
+ ui.add(multiline_textedit(self.state.str_mut("picture")));
});
in_frame(ui, |ui| {
ui.add(label("Banner"));
- ui.add(multiline_textedit(&mut self.state.banner));
+ ui.add(multiline_textedit(self.state.str_mut("banner")));
});
in_frame(ui, |ui| {
ui.add(label("About"));
- ui.add(multiline_textedit(&mut self.state.about));
+ ui.add(multiline_textedit(self.state.str_mut("about")));
});
in_frame(ui, |ui| {
ui.add(label("Website"));
- ui.add(singleline_textedit(&mut self.state.website));
+ ui.add(singleline_textedit(self.state.str_mut("website")));
});
in_frame(ui, |ui| {
ui.add(label("Lightning network address (lud16)"));
- ui.add(multiline_textedit(&mut self.state.lud16));
+ ui.add(multiline_textedit(self.state.str_mut("lud16")));
});
in_frame(ui, |ui| {
ui.add(label("Nostr address (NIP-05 identity)"));
- 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 identification", suffix)
- } else {
- format!(
- "\"{}\" at \"{}\" will be used for identification",
- prefix, suffix
- )
- }),
- );
- }
- }
+ ui.add(singleline_textedit(self.state.str_mut("nip05")));
+
+ let Some(nip05) = self.state.nip05() else {
+ return;
+ };
+
+ let mut split = nip05.split('@');
+
+ let Some(prefix) = split.next() else {
+ return;
+ };
+ let Some(suffix) = split.next() else {
+ return;
+ };
+
+ 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 identification", suffix)
+ } else {
+ format!(
+ "\"{}\" at \"{}\" will be used for identification",
+ prefix, suffix
+ )
+ }),
+ );
});
}
}
@@ -165,46 +168,3 @@ fn button(text: &str, width: f32) -> egui::Button<'static> {
.corner_radius(CornerRadius::same(8))
.min_size(vec2(width, 40.0))
}
-
-mod preview {
- use notedeck::{App, AppAction};
-
- 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,
- ) -> Option<AppAction> {
- EditProfileView::new(&mut self.state, ctx.img_cache).ui(ui);
- None
- }
- }
-
- impl Preview for EditProfileView<'_> {
- type Prev = EditProfilePreivew;
-
- fn preview(_cfg: PreviewConfig) -> Self::Prev {
- EditProfilePreivew::default()
- }
- }
-}
diff --git a/crates/notedeck_columns/src/view_state.rs b/crates/notedeck_columns/src/view_state.rs
@@ -4,8 +4,8 @@ use enostr::Pubkey;
use crate::deck_state::DeckState;
use crate::login_manager::AcquireKeyState;
-use crate::profile_state::ProfileState;
use crate::ui::search::SearchQueryState;
+use enostr::ProfileState;
/// Various state for views
#[derive(Default)]