thread.rs (11410B)
1 use egui::InnerResponse; 2 use egui_virtual_list::VirtualList; 3 use nostrdb::{Note, Transaction}; 4 use notedeck::note::root_note_id_from_selected_id; 5 use notedeck::{NoteAction, NoteContext}; 6 use notedeck_ui::note::NoteResponse; 7 use notedeck_ui::{NoteOptions, NoteView}; 8 9 use crate::timeline::thread::{NoteSeenFlags, ParentState, Threads}; 10 use notedeck::DragResponse; 11 12 pub struct ThreadView<'a, 'd> { 13 threads: &'a mut Threads, 14 selected_note_id: &'a [u8; 32], 15 note_options: NoteOptions, 16 col: usize, 17 note_context: &'a mut NoteContext<'d>, 18 } 19 20 impl<'a, 'd> ThreadView<'a, 'd> { 21 #[allow(clippy::too_many_arguments)] 22 pub fn new( 23 threads: &'a mut Threads, 24 selected_note_id: &'a [u8; 32], 25 note_options: NoteOptions, 26 note_context: &'a mut NoteContext<'d>, 27 col: usize, 28 ) -> Self { 29 ThreadView { 30 threads, 31 selected_note_id, 32 note_options, 33 note_context, 34 col, 35 } 36 } 37 38 pub fn scroll_id(selected_note_id: &[u8; 32], col: usize) -> egui::Id { 39 egui::Id::new(("threadscroll", selected_note_id, col)) 40 } 41 42 pub fn ui(&mut self, ui: &mut egui::Ui) -> DragResponse<NoteAction> { 43 let txn = Transaction::new(self.note_context.ndb).expect("txn"); 44 45 let scroll_id = ThreadView::scroll_id(self.selected_note_id, self.col); 46 let mut scroll_area = egui::ScrollArea::vertical() 47 .id_salt(scroll_id) 48 .animated(false) 49 .auto_shrink([false, false]) 50 .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible); 51 52 if let Some(thread) = self.threads.threads.get_mut(&self.selected_note_id) { 53 if let Some(new_offset) = thread.set_scroll_offset.take() { 54 scroll_area = scroll_area.vertical_scroll_offset(new_offset); 55 } 56 } 57 58 let output = scroll_area.show(ui, |ui| self.notes(ui, &txn)); 59 60 let out_id = output.id; 61 let mut resp = output.inner; 62 63 if let Some(NoteAction::Note { 64 note_id: _, 65 preview: _, 66 scroll_offset, 67 }) = &mut resp 68 { 69 *scroll_offset = output.state.offset.y; 70 } 71 72 DragResponse::output(resp).scroll_raw(out_id) 73 } 74 75 fn notes(&mut self, ui: &mut egui::Ui, txn: &Transaction) -> Option<NoteAction> { 76 let Ok(cur_note) = self 77 .note_context 78 .ndb 79 .get_note_by_id(txn, self.selected_note_id) 80 else { 81 let id = *self.selected_note_id; 82 tracing::error!("ndb: Did not find note {}", enostr::NoteId::new(id).hex()); 83 return None; 84 }; 85 86 self.threads.update( 87 &cur_note, 88 self.note_context.note_cache, 89 self.note_context.ndb, 90 txn, 91 self.note_context.unknown_ids, 92 self.col, 93 ); 94 95 let cur_node = self.threads.threads.get(&self.selected_note_id).unwrap(); 96 97 let full_chain = cur_node.have_all_ancestors; 98 let mut note_builder = ThreadNoteBuilder::new(cur_note); 99 100 let mut parent_state = cur_node.prev.clone(); 101 while let ParentState::Parent(id) = parent_state { 102 if let Ok(note) = self.note_context.ndb.get_note_by_id(txn, id.bytes()) { 103 note_builder.add_chain(note); 104 if let Some(res) = self.threads.threads.get(&id.bytes()) { 105 parent_state = res.prev.clone(); 106 continue; 107 } 108 } 109 parent_state = ParentState::Unknown; 110 } 111 112 for note_ref in cur_node.replies.values() { 113 if let Ok(note) = self.note_context.ndb.get_note_by_key(txn, note_ref.key) { 114 note_builder.add_reply(note); 115 } 116 } 117 118 let list = &mut self 119 .threads 120 .threads 121 .get_mut(&self.selected_note_id) 122 .unwrap() 123 .list; 124 125 let notes = note_builder.into_notes( 126 self.note_options.contains(NoteOptions::RepliesNewestFirst), 127 &mut self.threads.seen_flags, 128 ); 129 130 if !full_chain { 131 // TODO(kernelkind): insert UI denoting we don't have the full chain yet 132 ui.colored_label(ui.visuals().error_fg_color, "LOADING NOTES"); 133 } 134 135 show_notes(ui, list, ¬es, self.note_context, self.note_options, txn) 136 } 137 } 138 139 #[allow(clippy::too_many_arguments)] 140 fn show_notes( 141 ui: &mut egui::Ui, 142 list: &mut VirtualList, 143 thread_notes: &ThreadNotes, 144 note_context: &mut NoteContext<'_>, 145 flags: NoteOptions, 146 txn: &Transaction, 147 ) -> Option<NoteAction> { 148 let mut action = None; 149 150 ui.spacing_mut().item_spacing.y = 0.0; 151 ui.spacing_mut().item_spacing.x = 4.0; 152 153 let selected_note_index = thread_notes.selected_index; 154 let notes = &thread_notes.notes; 155 156 let is_muted = note_context.accounts.mutefun(); 157 158 list.ui_custom_layout(ui, notes.len(), |ui, cur_index| { 159 let note = ¬es[cur_index]; 160 161 // should we mute the thread? we might not have it! 162 let muted = root_note_id_from_selected_id( 163 note_context.ndb, 164 note_context.note_cache, 165 txn, 166 note.note.id(), 167 ) 168 .ok() 169 .is_some_and(|root_id| is_muted(¬e.note, root_id.bytes())); 170 171 if muted { 172 return 1; 173 } 174 175 let resp = note.show(note_context, flags, ui); 176 177 action = if cur_index == selected_note_index { 178 resp.action.and_then(strip_note_action) 179 } else { 180 resp.action 181 } 182 .or(action.take()); 183 184 1 185 }); 186 187 action 188 } 189 190 fn strip_note_action(action: NoteAction) -> Option<NoteAction> { 191 if matches!( 192 action, 193 NoteAction::Note { 194 note_id: _, 195 preview: false, 196 scroll_offset: _, 197 } 198 ) { 199 return None; 200 } 201 202 Some(action) 203 } 204 205 struct ThreadNoteBuilder<'a> { 206 chain: Vec<Note<'a>>, 207 selected: Note<'a>, 208 replies: Vec<Note<'a>>, 209 } 210 211 impl<'a> ThreadNoteBuilder<'a> { 212 pub fn new(selected: Note<'a>) -> Self { 213 Self { 214 chain: Vec::new(), 215 selected, 216 replies: Vec::new(), 217 } 218 } 219 220 pub fn add_chain(&mut self, note: Note<'a>) { 221 self.chain.push(note); 222 } 223 224 pub fn add_reply(&mut self, note: Note<'a>) { 225 self.replies.push(note); 226 } 227 228 pub fn into_notes( 229 mut self, 230 replies_newer_first: bool, 231 seen_flags: &mut NoteSeenFlags, 232 ) -> ThreadNotes<'a> { 233 let mut notes = Vec::new(); 234 235 let selected_is_root = self.chain.is_empty(); 236 let mut cur_is_root = true; 237 while let Some(note) = self.chain.pop() { 238 notes.push(ThreadNote { 239 unread_and_have_replies: *seen_flags.get(note.id()).unwrap_or(&false), 240 note, 241 note_type: ThreadNoteType::Chain { root: cur_is_root }, 242 }); 243 cur_is_root = false; 244 } 245 246 let selected_index = notes.len(); 247 notes.push(ThreadNote { 248 note: self.selected, 249 note_type: ThreadNoteType::Selected { 250 root: selected_is_root, 251 }, 252 unread_and_have_replies: false, 253 }); 254 255 if replies_newer_first { 256 self.replies 257 .sort_by_key(|b| std::cmp::Reverse(b.created_at())); 258 } 259 260 for reply in self.replies { 261 notes.push(ThreadNote { 262 unread_and_have_replies: *seen_flags.get(reply.id()).unwrap_or(&false), 263 note: reply, 264 note_type: ThreadNoteType::Reply, 265 }); 266 } 267 268 ThreadNotes { 269 notes, 270 selected_index, 271 } 272 } 273 } 274 275 enum ThreadNoteType { 276 Chain { root: bool }, 277 Selected { root: bool }, 278 Reply, 279 } 280 281 impl ThreadNoteType { 282 fn is_selected(&self) -> bool { 283 matches!(self, ThreadNoteType::Selected { .. }) 284 } 285 } 286 287 struct ThreadNotes<'a> { 288 notes: Vec<ThreadNote<'a>>, 289 selected_index: usize, 290 } 291 292 struct ThreadNote<'a> { 293 pub note: Note<'a>, 294 note_type: ThreadNoteType, 295 pub unread_and_have_replies: bool, 296 } 297 298 impl<'a> ThreadNote<'a> { 299 fn options(&self, mut cur_options: NoteOptions) -> NoteOptions { 300 match self.note_type { 301 ThreadNoteType::Chain { root: _ } => cur_options, 302 ThreadNoteType::Selected { root: _ } => { 303 cur_options.set(NoteOptions::Wide, true); 304 cur_options.set(NoteOptions::SelectableText, true); 305 cur_options.set(NoteOptions::FullCreatedDate, true); 306 cur_options 307 } 308 ThreadNoteType::Reply => cur_options, 309 } 310 } 311 312 fn show( 313 &self, 314 note_context: &'a mut NoteContext<'_>, 315 flags: NoteOptions, 316 ui: &mut egui::Ui, 317 ) -> NoteResponse { 318 let inner = notedeck_ui::padding(8.0, ui, |ui| { 319 NoteView::new(note_context, &self.note, self.options(flags)) 320 .selected_style(self.note_type.is_selected()) 321 .unread_indicator(self.unread_and_have_replies) 322 .show(ui) 323 }); 324 325 match self.note_type { 326 ThreadNoteType::Chain { root } => add_chain_adornment(ui, &inner, root), 327 ThreadNoteType::Selected { root } => add_selected_adornment(ui, &inner, root), 328 ThreadNoteType::Reply => notedeck_ui::hline(ui), 329 } 330 331 inner.inner 332 } 333 } 334 335 fn add_chain_adornment(ui: &mut egui::Ui, note_resp: &InnerResponse<NoteResponse>, root: bool) { 336 let Some(pfp_rect) = note_resp.inner.pfp_rect else { 337 return; 338 }; 339 340 let note_rect = note_resp.response.rect; 341 342 let painter = ui.painter_at(note_rect); 343 344 if !root { 345 paint_line_above_pfp(ui, &painter, &pfp_rect, ¬e_rect); 346 } 347 348 // painting line below pfp: 349 let top_pt = { 350 let mut top = pfp_rect.center(); 351 top.y = pfp_rect.bottom(); 352 top 353 }; 354 355 let bottom_pt = { 356 let mut bottom = top_pt; 357 bottom.y = note_rect.bottom(); 358 bottom 359 }; 360 361 painter.line_segment([top_pt, bottom_pt], LINE_STROKE(ui)); 362 363 let hline_min_x = top_pt.x + 6.0; 364 notedeck_ui::hline_with_width( 365 ui, 366 egui::Rangef::new(hline_min_x, ui.available_rect_before_wrap().right()), 367 ); 368 } 369 370 fn add_selected_adornment(ui: &mut egui::Ui, note_resp: &InnerResponse<NoteResponse>, root: bool) { 371 let Some(pfp_rect) = note_resp.inner.pfp_rect else { 372 return; 373 }; 374 let note_rect = note_resp.response.rect; 375 let painter = ui.painter_at(note_rect); 376 377 if !root { 378 paint_line_above_pfp(ui, &painter, &pfp_rect, ¬e_rect); 379 } 380 notedeck_ui::hline(ui); 381 } 382 383 fn paint_line_above_pfp( 384 ui: &egui::Ui, 385 painter: &egui::Painter, 386 pfp_rect: &egui::Rect, 387 note_rect: &egui::Rect, 388 ) { 389 let bottom_pt = { 390 let mut center = pfp_rect.center(); 391 center.y = pfp_rect.top(); 392 center 393 }; 394 395 let top_pt = { 396 let mut top = bottom_pt; 397 top.y = note_rect.top(); 398 top 399 }; 400 401 painter.line_segment([bottom_pt, top_pt], LINE_STROKE(ui)); 402 } 403 404 const LINE_STROKE: fn(&egui::Ui) -> egui::Stroke = |ui: &egui::Ui| { 405 let mut stroke = ui.style().visuals.widgets.noninteractive.bg_stroke; 406 stroke.width = 2.0; 407 stroke 408 };