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