profile.rs (10185B)
1 use enostr::{FilledKeypair, FullKeypair, ProfileState, Pubkey}; 2 use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder, Transaction}; 3 4 use notedeck::{ 5 builder_from_note, note::publish::publish_note_builder, send_mute_event, Accounts, 6 ContactState, DataPath, Localization, ProfileContext, PublishApi, RelayType, RemoteApi, 7 }; 8 use tracing::info; 9 10 use crate::{column::Column, nav::RouterAction, route::Route, storage, Damus}; 11 12 pub struct SaveProfileChanges { 13 pub kp: FullKeypair, 14 pub state: ProfileState, 15 } 16 17 impl SaveProfileChanges { 18 pub fn new(kp: FullKeypair, state: ProfileState) -> Self { 19 Self { kp, state } 20 } 21 pub fn to_note(&self) -> Note<'_> { 22 let sec = &self.kp.secret_key.to_secret_bytes(); 23 add_client_tag(NoteBuilder::new()) 24 .kind(0) 25 .content(&self.state.to_json()) 26 .options(NoteBuildOptions::default().created_at(true).sign(sec)) 27 .build() 28 .expect("should build") 29 } 30 } 31 32 fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> { 33 builder 34 .start_tag() 35 .tag_str("client") 36 .tag_str("Damus Notedeck") 37 } 38 39 pub enum ProfileAction { 40 Edit(FullKeypair), 41 SaveChanges(SaveProfileChanges), 42 Follow(Pubkey), 43 Unfollow(Pubkey), 44 Context(ProfileContext), 45 } 46 47 impl ProfileAction { 48 #[allow(clippy::too_many_arguments)] 49 pub fn process_profile_action( 50 &self, 51 app: &mut Damus, 52 path: &DataPath, 53 i18n: &mut Localization, 54 ctx: &egui::Context, 55 ndb: &Ndb, 56 remote: &mut RemoteApi<'_>, 57 accounts: &Accounts, 58 ) -> Option<RouterAction> { 59 match self { 60 ProfileAction::Edit(kp) => Some(RouterAction::route_to(Route::EditProfile(kp.pubkey))), 61 ProfileAction::SaveChanges(changes) => { 62 let note = changes.to_note(); 63 let Ok(event) = enostr::ClientMessage::event(¬e) else { 64 tracing::error!("could not serialize profile note?"); 65 return None; 66 }; 67 68 let Ok(json) = event.to_json() else { 69 tracing::error!("could not serialize profile note?"); 70 return None; 71 }; 72 73 // TODO(jb55): do this in a more centralized place 74 let _ = ndb.process_event_with(&json, nostrdb::IngestMetadata::new().client(true)); 75 76 info!("sending {}", &json); 77 let mut publisher = remote.publisher(accounts); 78 publisher.publish_note(¬e, RelayType::AccountsWrite); 79 80 Some(RouterAction::GoBack) 81 } 82 ProfileAction::Follow(target_key) => { 83 let mut publisher = remote.publisher(accounts); 84 Self::send_follow_user_event(ndb, &mut publisher, accounts, target_key); 85 None 86 } 87 ProfileAction::Unfollow(target_key) => { 88 let mut publisher = remote.publisher(accounts); 89 Self::send_unfollow_user_event(ndb, &mut publisher, accounts, target_key); 90 None 91 } 92 ProfileAction::Context(profile_context) => { 93 use notedeck::ProfileContextSelection; 94 match &profile_context.selection { 95 ProfileContextSelection::ViewAs => { 96 Some(RouterAction::SwitchAccount(profile_context.profile)) 97 } 98 ProfileContextSelection::AddProfileColumn => { 99 let timeline_route = Route::Timeline( 100 crate::timeline::TimelineKind::Profile(profile_context.profile), 101 ); 102 103 let missing_column = { 104 let deck_columns = app.columns(accounts).columns(); 105 let router_head = std::slice::from_ref(&timeline_route); 106 !deck_columns 107 .iter() 108 .any(|column| column.router.routes().starts_with(router_head)) 109 }; 110 111 if missing_column { 112 let column = Column::new(vec![timeline_route]); 113 114 app.columns_mut(i18n, accounts).add_column(column); 115 116 storage::save_decks_cache(path, &app.decks_cache); 117 } 118 119 None 120 } 121 ProfileContextSelection::MuteUser => { 122 let kp = accounts.get_selected_account().key.to_full()?; 123 let muted = accounts.mute(); 124 let txn = Transaction::new(ndb).expect("txn"); 125 let publisher = &mut remote.publisher(accounts); 126 if muted.is_pk_muted(profile_context.profile.bytes()) { 127 notedeck::send_unmute_event( 128 ndb, 129 &txn, 130 publisher, 131 kp, 132 &muted, 133 &profile_context.profile, 134 ); 135 } else { 136 send_mute_event( 137 ndb, 138 &txn, 139 publisher, 140 kp, 141 &muted, 142 &profile_context.profile, 143 ); 144 } 145 None 146 } 147 ProfileContextSelection::ReportUser => { 148 let target = notedeck::ReportTarget { 149 pubkey: profile_context.profile, 150 note_id: None, 151 }; 152 Some(RouterAction::route_to_sheet( 153 Route::Report(target), 154 egui_nav::Split::AbsoluteFromBottom(340.0), 155 )) 156 } 157 _ => { 158 profile_context 159 .selection 160 .process(ctx, &profile_context.profile); 161 None 162 } 163 } 164 } 165 } 166 } 167 168 fn send_follow_user_event( 169 ndb: &Ndb, 170 publisher: &mut PublishApi<'_, '_>, 171 accounts: &Accounts, 172 target_key: &Pubkey, 173 ) { 174 send_kind_3_event(ndb, publisher, accounts, FollowAction::Follow(target_key)); 175 } 176 177 fn send_unfollow_user_event( 178 ndb: &Ndb, 179 publisher: &mut PublishApi<'_, '_>, 180 accounts: &Accounts, 181 target_key: &Pubkey, 182 ) { 183 send_kind_3_event(ndb, publisher, accounts, FollowAction::Unfollow(target_key)); 184 } 185 } 186 187 enum FollowAction<'a> { 188 Follow(&'a Pubkey), 189 Unfollow(&'a Pubkey), 190 } 191 192 fn send_kind_3_event( 193 ndb: &Ndb, 194 publisher: &mut PublishApi<'_, '_>, 195 accounts: &Accounts, 196 action: FollowAction, 197 ) { 198 let Some(kp) = accounts.get_selected_account().key.to_full() else { 199 return; 200 }; 201 202 let txn = Transaction::new(ndb).expect("txn"); 203 204 let ContactState::Received { 205 contacts: _, 206 note_key, 207 timestamp: _, 208 } = accounts.get_selected_account().data.contacts.get_state() 209 else { 210 return; 211 }; 212 213 let contact_note = match ndb.get_note_by_key(&txn, *note_key).ok() { 214 Some(n) => n, 215 None => { 216 tracing::error!( 217 "Somehow we are in state ContactState::Received but the contact note key doesn't exist" 218 ); 219 return; 220 } 221 }; 222 223 if contact_note.kind() != 3 { 224 tracing::error!( 225 "Something very wrong just occured. The key for the supposed contact note yielded a note which was not a contact..." 226 ); 227 return; 228 } 229 230 let builder = match action { 231 FollowAction::Follow(pubkey) => { 232 builder_from_note(contact_note, None::<fn(&nostrdb::Tag<'_>) -> bool>) 233 .start_tag() 234 .tag_str("p") 235 .tag_str(&pubkey.hex()) 236 } 237 FollowAction::Unfollow(pubkey) => builder_from_note( 238 contact_note, 239 Some(|tag: &nostrdb::Tag<'_>| { 240 if tag.count() < 2 { 241 return false; 242 } 243 244 let Some("p") = tag.get_str(0) else { 245 return false; 246 }; 247 248 let Some(cur_val) = tag.get_id(1) else { 249 return false; 250 }; 251 252 cur_val == pubkey.bytes() 253 }), 254 ), 255 }; 256 257 publish_note_builder(builder, ndb, publisher, kp); 258 } 259 260 pub fn send_new_contact_list( 261 kp: FilledKeypair, 262 ndb: &Ndb, 263 publisher: &mut PublishApi<'_, '_>, 264 mut pks_to_follow: Vec<Pubkey>, 265 ) { 266 if !pks_to_follow.contains(kp.pubkey) { 267 pks_to_follow.push(*kp.pubkey); 268 } 269 270 let builder = construct_new_contact_list(pks_to_follow); 271 272 publish_note_builder(builder, ndb, publisher, kp); 273 } 274 275 fn construct_new_contact_list<'a>(pks: Vec<Pubkey>) -> NoteBuilder<'a> { 276 let mut builder = NoteBuilder::new() 277 .content("") 278 .kind(3) 279 .options(NoteBuildOptions::default()); 280 281 for pk in pks { 282 builder = builder.start_tag().tag_str("p").tag_str(&pk.hex()); 283 } 284 285 builder 286 } 287 288 pub fn send_default_dms_relay_list( 289 kp: FilledKeypair<'_>, 290 ndb: &Ndb, 291 publisher: &mut PublishApi<'_, '_>, 292 ) { 293 publish_note_builder(construct_default_dms_relay_list(), ndb, publisher, kp); 294 } 295 296 fn construct_default_dms_relay_list<'a>() -> NoteBuilder<'a> { 297 let mut builder = NoteBuilder::new() 298 .content("") 299 .kind(10050) 300 .options(NoteBuildOptions::default()); 301 302 for relay in default_dms_relays() { 303 builder = builder.start_tag().tag_str("relay").tag_str(relay); 304 } 305 306 builder 307 } 308 309 fn default_dms_relays() -> Vec<&'static str> { 310 vec!["wss://relay.damus.io", "wss://nos.lol"] 311 }