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