notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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(&note) 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(&note, 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 }