notedeck

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

lib.rs (5861B)


      1 pub mod cache;
      2 pub mod convo_renderable;
      3 pub mod nav;
      4 pub mod nip17;
      5 pub mod ui;
      6 
      7 use enostr::Pubkey;
      8 use hashbrown::HashMap;
      9 use nav::{process_messages_ui_response, Route};
     10 use nostrdb::{Subscription, Transaction};
     11 use notedeck::{
     12     try_process_events_core, ui::is_narrow, Accounts, App, AppContext, AppResponse, Router,
     13 };
     14 
     15 use crate::{
     16     cache::{ConversationCache, ConversationListState, ConversationStates},
     17     nip17::conversation_filter,
     18     ui::{login_nsec_prompt, messages::messages_ui},
     19 };
     20 
     21 pub struct MessagesApp {
     22     messages: ConversationsCtx,
     23     states: ConversationStates,
     24     router: Router<Route>,
     25 }
     26 
     27 impl MessagesApp {
     28     pub fn new() -> Self {
     29         Self {
     30             messages: ConversationsCtx::default(),
     31             states: ConversationStates::default(),
     32             router: Router::new(vec![Route::ConvoList]),
     33         }
     34     }
     35 }
     36 
     37 impl Default for MessagesApp {
     38     fn default() -> Self {
     39         Self::new()
     40     }
     41 }
     42 
     43 impl App for MessagesApp {
     44     #[profiling::function]
     45     fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
     46         try_process_events_core(ctx, ui.ctx(), |_, _| {});
     47 
     48         let Some(cache) = self.messages.get_current_mut(ctx.accounts) else {
     49             login_nsec_prompt(ui, ctx.i18n);
     50             return AppResponse::none();
     51         };
     52 
     53         's: {
     54             let Some(secret) = &ctx.accounts.get_selected_account().key.secret_key else {
     55                 break 's;
     56             };
     57 
     58             ctx.ndb.add_key(&secret.secret_bytes());
     59 
     60             let giftwrap_ndb = ctx.ndb.clone();
     61             let r = std::thread::Builder::new()
     62                 .name("process_giftwraps".into())
     63                 .spawn(move || {
     64                     let txn = Transaction::new(&giftwrap_ndb).expect("txn");
     65                     // although the actual giftwrap processing happens on the ingestion pool, this
     66                     // function still looks up giftwraps to process on the main thread, which can
     67                     // cause a freeze.
     68                     //
     69                     // TODO(jb55): move the giftwrap query logic into the internal threadpool so we
     70                     // don't have to spawn a thread here
     71                     giftwrap_ndb.process_giftwraps(&txn);
     72                 });
     73 
     74             if let Err(err) = r {
     75                 tracing::error!("failed to spawn process_giftwraps thread: {err}");
     76             }
     77         }
     78 
     79         match cache.state {
     80             ConversationListState::Initializing => initialize(ctx, cache, is_narrow(ui.ctx())),
     81             ConversationListState::Initialized(subscription) => 's: {
     82                 let Some(sub) = subscription else {
     83                     break 's;
     84                 };
     85                 update_initialized(ctx, cache, sub);
     86             }
     87         }
     88 
     89         let selected_pubkey = ctx.accounts.selected_account_pubkey();
     90 
     91         let contacts_state = ctx
     92             .accounts
     93             .get_selected_account()
     94             .data
     95             .contacts
     96             .get_state();
     97         let resp = messages_ui(
     98             cache,
     99             &mut self.states,
    100             ctx.media_jobs.sender(),
    101             ctx.ndb,
    102             selected_pubkey,
    103             ui,
    104             ctx.img_cache,
    105             &self.router,
    106             ctx.settings.get_settings_mut(),
    107             contacts_state,
    108             ctx.i18n,
    109         );
    110         let action =
    111             process_messages_ui_response(resp, ctx, cache, &mut self.router, is_narrow(ui.ctx()));
    112 
    113         AppResponse::action(action)
    114     }
    115 }
    116 
    117 #[profiling::function]
    118 fn initialize(ctx: &mut AppContext, cache: &mut ConversationCache, is_narrow: bool) {
    119     let txn = Transaction::new(ctx.ndb).expect("txn");
    120     cache.init_conversations(
    121         ctx.ndb,
    122         &txn,
    123         ctx.accounts.selected_account_pubkey(),
    124         &mut *ctx.note_cache,
    125         &mut *ctx.unknown_ids,
    126     );
    127     if !is_narrow {
    128         if let Some(first) = cache.first_convo_id() {
    129             cache.open_conversation(
    130                 ctx.ndb,
    131                 &txn,
    132                 first,
    133                 ctx.note_cache,
    134                 ctx.unknown_ids,
    135                 ctx.accounts.selected_account_pubkey(),
    136             );
    137         }
    138     }
    139     let sub = match ctx
    140         .ndb
    141         .subscribe(&conversation_filter(ctx.accounts.selected_account_pubkey()))
    142     {
    143         Ok(sub) => Some(sub),
    144         Err(e) => {
    145             tracing::error!("couldn't sub ndb: {e}");
    146             None
    147         }
    148     };
    149 
    150     cache.state = ConversationListState::Initialized(sub);
    151 }
    152 
    153 #[profiling::function]
    154 fn update_initialized(ctx: &mut AppContext, cache: &mut ConversationCache, sub: Subscription) {
    155     let notes = ctx.ndb.poll_for_notes(sub, 10);
    156     let txn = Transaction::new(ctx.ndb).expect("txn");
    157     for key in notes {
    158         let note = match ctx.ndb.get_note_by_key(&txn, key) {
    159             Ok(n) => n,
    160             Err(e) => {
    161                 tracing::error!("could not find note key: {e}");
    162                 continue;
    163             }
    164         };
    165         cache.ingest_chatroom_msg(note, key, ctx.ndb, &txn, ctx.note_cache, ctx.unknown_ids);
    166     }
    167 }
    168 
    169 /// Storage for conversations per account. Account management is performed by `Accounts`
    170 #[derive(Default)]
    171 struct ConversationsCtx {
    172     convos_per_acc: HashMap<Pubkey, ConversationCache>,
    173 }
    174 
    175 impl ConversationsCtx {
    176     /// Get the conversation cache for the selected account. Return None if we don't have a full kp
    177     pub fn get_current_mut(&mut self, accounts: &Accounts) -> Option<&mut ConversationCache> {
    178         accounts.get_selected_account().keypair().secret_key?;
    179 
    180         let current = accounts.selected_account_pubkey();
    181         Some(
    182             self.convos_per_acc
    183                 .raw_entry_mut()
    184                 .from_key(current)
    185                 .or_insert_with(|| (*current, ConversationCache::new()))
    186                 .1,
    187         )
    188     }
    189 }