actionbar.rs (12045B)
1 use crate::{ 2 column::Columns, 3 nav::{RouterAction, RouterType}, 4 route::Route, 5 timeline::{ 6 thread::{ 7 selected_has_at_least_n_replies, InsertionResponse, NoteSeenFlags, ThreadNode, Threads, 8 }, 9 ThreadSelection, TimelineCache, TimelineKind, 10 }, 11 }; 12 13 use enostr::{NoteId, Pubkey, RelayPool}; 14 use nostrdb::{Ndb, NoteKey, Transaction}; 15 use notedeck::{ 16 get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache, 17 NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps, 18 }; 19 use tracing::error; 20 21 pub struct NewNotes { 22 pub id: TimelineKind, 23 pub notes: Vec<NoteKey>, 24 } 25 26 pub enum NotesOpenResult { 27 Timeline(TimelineOpenResult), 28 Thread(NewThreadNotes), 29 } 30 31 pub enum TimelineOpenResult { 32 NewNotes(NewNotes), 33 } 34 35 struct NoteActionResponse { 36 timeline_res: Option<NotesOpenResult>, 37 router_action: Option<RouterAction>, 38 } 39 40 /// The note action executor for notedeck_columns 41 #[allow(clippy::too_many_arguments)] 42 fn execute_note_action( 43 action: NoteAction, 44 ndb: &mut Ndb, 45 timeline_cache: &mut TimelineCache, 46 threads: &mut Threads, 47 note_cache: &mut NoteCache, 48 pool: &mut RelayPool, 49 txn: &Transaction, 50 accounts: &mut Accounts, 51 global_wallet: &mut GlobalWallet, 52 zaps: &mut Zaps, 53 images: &mut Images, 54 router_type: RouterType, 55 ui: &mut egui::Ui, 56 col: usize, 57 ) -> NoteActionResponse { 58 let mut timeline_res = None; 59 let mut router_action = None; 60 let can_post = accounts.get_selected_account().key.secret_key.is_some(); 61 62 match action { 63 NoteAction::Scroll(ref scroll_info) => { 64 tracing::trace!("timeline scroll {scroll_info:?}") 65 } 66 67 NoteAction::Reply(note_id) => { 68 if can_post { 69 router_action = Some(RouterAction::route_to(Route::reply(note_id))); 70 } else { 71 router_action = Some(RouterAction::route_to(Route::accounts())); 72 } 73 } 74 NoteAction::Profile(pubkey) => { 75 let kind = TimelineKind::Profile(pubkey); 76 router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone()))); 77 timeline_res = timeline_cache 78 .open(ndb, note_cache, txn, pool, &kind) 79 .map(NotesOpenResult::Timeline); 80 } 81 NoteAction::Note { note_id, preview } => 'ex: { 82 let Ok(thread_selection) = ThreadSelection::from_note_id(ndb, note_cache, txn, note_id) 83 else { 84 tracing::error!("No thread selection for {}?", hex::encode(note_id.bytes())); 85 break 'ex; 86 }; 87 88 timeline_res = threads 89 .open(ndb, txn, pool, &thread_selection, preview, col) 90 .map(NotesOpenResult::Thread); 91 92 let route = Route::Thread(thread_selection); 93 94 router_action = Some(RouterAction::Overlay { 95 route, 96 make_new: preview, 97 }); 98 } 99 NoteAction::Hashtag(htag) => { 100 let kind = TimelineKind::Hashtag(vec![htag.clone()]); 101 router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone()))); 102 timeline_res = timeline_cache 103 .open(ndb, note_cache, txn, pool, &kind) 104 .map(NotesOpenResult::Timeline); 105 } 106 NoteAction::Quote(note_id) => { 107 if can_post { 108 router_action = Some(RouterAction::route_to(Route::quote(note_id))); 109 } else { 110 router_action = Some(RouterAction::route_to(Route::accounts())); 111 } 112 } 113 NoteAction::Zap(zap_action) => { 114 let cur_acc = accounts.get_selected_account(); 115 116 let sender = cur_acc.key.pubkey; 117 118 match &zap_action { 119 ZapAction::Send(target) => 'a: { 120 let Some(wallet) = get_wallet_for(accounts, global_wallet, sender.bytes()) 121 else { 122 zaps.send_error( 123 sender.bytes(), 124 ZapTarget::Note((&target.target).into()), 125 ZappingError::SenderNoWallet, 126 ); 127 break 'a; 128 }; 129 130 if let RouterType::Sheet = router_type { 131 router_action = Some(RouterAction::GoBack); 132 } 133 134 send_zap( 135 &sender, 136 zaps, 137 pool, 138 target, 139 wallet.default_zap.get_default_zap_msats(), 140 ) 141 } 142 ZapAction::ClearError(target) => clear_zap_error(&sender, zaps, target), 143 ZapAction::CustomizeAmount(target) => { 144 let route = Route::CustomizeZapAmount(target.to_owned()); 145 router_action = Some(RouterAction::route_to_sheet(route)); 146 } 147 } 148 } 149 NoteAction::Context(context) => match ndb.get_note_by_key(txn, context.note_key) { 150 Err(err) => tracing::error!("{err}"), 151 Ok(note) => { 152 context.action.process(ui, ¬e, pool); 153 } 154 }, 155 NoteAction::Media(media_action) => { 156 media_action.process(images); 157 } 158 } 159 160 NoteActionResponse { 161 timeline_res, 162 router_action, 163 } 164 } 165 166 /// Execute a NoteAction and process the result 167 #[allow(clippy::too_many_arguments)] 168 pub fn execute_and_process_note_action( 169 action: NoteAction, 170 ndb: &mut Ndb, 171 columns: &mut Columns, 172 col: usize, 173 timeline_cache: &mut TimelineCache, 174 threads: &mut Threads, 175 note_cache: &mut NoteCache, 176 pool: &mut RelayPool, 177 txn: &Transaction, 178 unknown_ids: &mut UnknownIds, 179 accounts: &mut Accounts, 180 global_wallet: &mut GlobalWallet, 181 zaps: &mut Zaps, 182 images: &mut Images, 183 ui: &mut egui::Ui, 184 ) -> Option<RouterAction> { 185 let router_type = { 186 let sheet_router = &mut columns.column_mut(col).sheet_router; 187 188 if sheet_router.route().is_some() { 189 RouterType::Sheet 190 } else { 191 RouterType::Stack 192 } 193 }; 194 195 let resp = execute_note_action( 196 action, 197 ndb, 198 timeline_cache, 199 threads, 200 note_cache, 201 pool, 202 txn, 203 accounts, 204 global_wallet, 205 zaps, 206 images, 207 router_type, 208 ui, 209 col, 210 ); 211 212 if let Some(br) = resp.timeline_res { 213 match br { 214 NotesOpenResult::Timeline(timeline_open_result) => { 215 timeline_open_result.process(ndb, note_cache, txn, timeline_cache, unknown_ids); 216 } 217 NotesOpenResult::Thread(thread_open_result) => { 218 thread_open_result.process(threads, ndb, txn, unknown_ids, note_cache); 219 } 220 } 221 } 222 223 resp.router_action 224 } 225 226 fn send_zap( 227 sender: &Pubkey, 228 zaps: &mut Zaps, 229 pool: &RelayPool, 230 target_amount: &ZapTargetAmount, 231 default_msats: u64, 232 ) { 233 let zap_target = ZapTarget::Note((&target_amount.target).into()); 234 235 let msats = target_amount.specified_msats.unwrap_or(default_msats); 236 237 let sender_relays: Vec<String> = pool.relays.iter().map(|r| r.url().to_string()).collect(); 238 zaps.send_zap(sender.bytes(), sender_relays, zap_target, msats); 239 } 240 241 fn clear_zap_error(sender: &Pubkey, zaps: &mut Zaps, target: &NoteZapTargetOwned) { 242 zaps.clear_error_for(sender.bytes(), ZapTarget::Note(target.into())); 243 } 244 245 impl TimelineOpenResult { 246 pub fn new_notes(notes: Vec<NoteKey>, id: TimelineKind) -> Self { 247 Self::NewNotes(NewNotes::new(notes, id)) 248 } 249 250 pub fn process( 251 &self, 252 ndb: &Ndb, 253 note_cache: &mut NoteCache, 254 txn: &Transaction, 255 storage: &mut TimelineCache, 256 unknown_ids: &mut UnknownIds, 257 ) { 258 match self { 259 // update the thread for next render if we have new notes 260 TimelineOpenResult::NewNotes(new_notes) => { 261 new_notes.process(storage, ndb, txn, unknown_ids, note_cache); 262 } 263 } 264 } 265 } 266 267 impl NewNotes { 268 pub fn new(notes: Vec<NoteKey>, id: TimelineKind) -> Self { 269 NewNotes { notes, id } 270 } 271 272 /// Simple helper for processing a NewThreadNotes result. It simply 273 /// inserts/merges the notes into the corresponding timeline cache 274 pub fn process( 275 &self, 276 timeline_cache: &mut TimelineCache, 277 ndb: &Ndb, 278 txn: &Transaction, 279 unknown_ids: &mut UnknownIds, 280 note_cache: &mut NoteCache, 281 ) { 282 let reversed = false; 283 284 let timeline = if let Some(profile) = timeline_cache.get_mut(&self.id) { 285 profile 286 } else { 287 error!("NewNotes: could not get timeline for key {:?}", self.id); 288 return; 289 }; 290 291 if let Err(err) = timeline.insert(&self.notes, ndb, txn, unknown_ids, note_cache, reversed) 292 { 293 error!("error inserting notes into profile timeline: {err}") 294 } 295 } 296 } 297 298 pub struct NewThreadNotes { 299 pub selected_note_id: NoteId, 300 pub notes: Vec<NoteKey>, 301 } 302 303 impl NewThreadNotes { 304 pub fn process( 305 &self, 306 threads: &mut Threads, 307 ndb: &Ndb, 308 txn: &Transaction, 309 unknown_ids: &mut UnknownIds, 310 note_cache: &mut NoteCache, 311 ) { 312 let Some(node) = threads.threads.get_mut(&self.selected_note_id.bytes()) else { 313 tracing::error!("Could not find thread node for {:?}", self.selected_note_id); 314 return; 315 }; 316 317 process_thread_notes( 318 &self.notes, 319 node, 320 &mut threads.seen_flags, 321 ndb, 322 txn, 323 unknown_ids, 324 note_cache, 325 ); 326 } 327 } 328 329 pub fn process_thread_notes( 330 notes: &Vec<NoteKey>, 331 thread: &mut ThreadNode, 332 seen_flags: &mut NoteSeenFlags, 333 ndb: &Ndb, 334 txn: &Transaction, 335 unknown_ids: &mut UnknownIds, 336 note_cache: &mut NoteCache, 337 ) { 338 if notes.is_empty() { 339 return; 340 } 341 342 let mut has_spliced_resp = false; 343 let mut num_new_notes = 0; 344 for key in notes { 345 let note = if let Ok(note) = ndb.get_note_by_key(txn, *key) { 346 note 347 } else { 348 tracing::error!( 349 "hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", 350 key 351 ); 352 continue; 353 }; 354 355 // Ensure that unknown ids are captured when inserting notes 356 UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e); 357 358 let created_at = note.created_at(); 359 let note_ref = notedeck::NoteRef { 360 key: *key, 361 created_at, 362 }; 363 364 if thread.replies.contains(¬e_ref) { 365 continue; 366 } 367 368 let insertion_resp = thread.replies.insert(note_ref); 369 if let InsertionResponse::Merged(crate::timeline::MergeKind::Spliced) = insertion_resp { 370 has_spliced_resp = true; 371 } 372 373 if matches!(insertion_resp, InsertionResponse::Merged(_)) { 374 num_new_notes += 1; 375 } 376 377 if !seen_flags.contains(note.id()) { 378 let cached_note = note_cache.cached_note_or_insert_mut(*key, ¬e); 379 380 let note_reply = cached_note.reply.borrow(note.tags()); 381 382 let has_reply = if let Some(root) = note_reply.root() { 383 selected_has_at_least_n_replies(ndb, txn, Some(note.id()), root.id, 1) 384 } else { 385 selected_has_at_least_n_replies(ndb, txn, None, note.id(), 1) 386 }; 387 388 seen_flags.mark_replies(note.id(), has_reply); 389 } 390 } 391 392 if has_spliced_resp { 393 tracing::debug!( 394 "spliced when inserting {} new notes, resetting virtual list", 395 num_new_notes 396 ); 397 thread.list.reset(); 398 } 399 }