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 }