timeline.rs (8136B)
1 use crate::{actionbar::BarResult, draft::DraftSource, ui, ui::note::PostAction, Damus}; 2 use egui::containers::scroll_area::ScrollBarVisibility; 3 use egui::{Direction, Layout}; 4 use egui_tabs::TabColor; 5 use nostrdb::Transaction; 6 use tracing::{debug, info, warn}; 7 8 pub struct TimelineView<'a> { 9 app: &'a mut Damus, 10 reverse: bool, 11 timeline: usize, 12 } 13 14 impl<'a> TimelineView<'a> { 15 pub fn new(app: &'a mut Damus, timeline: usize) -> TimelineView<'a> { 16 let reverse = false; 17 TimelineView { 18 app, 19 timeline, 20 reverse, 21 } 22 } 23 24 pub fn ui(&mut self, ui: &mut egui::Ui) { 25 timeline_ui(ui, self.app, self.timeline, self.reverse); 26 } 27 28 pub fn reversed(mut self) -> Self { 29 self.reverse = true; 30 self 31 } 32 } 33 34 fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bool) { 35 //padding(4.0, ui, |ui| ui.heading("Notifications")); 36 /* 37 let font_id = egui::TextStyle::Body.resolve(ui.style()); 38 let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y; 39 */ 40 41 if timeline == 0 { 42 postbox_view(app, ui); 43 } 44 45 app.timelines[timeline].selected_view = tabs_ui(ui); 46 47 // need this for some reason?? 48 ui.add_space(3.0); 49 50 let scroll_id = egui::Id::new(("tlscroll", app.timelines[timeline].selected_view, timeline)); 51 egui::ScrollArea::vertical() 52 .id_source(scroll_id) 53 .animated(false) 54 .auto_shrink([false, false]) 55 .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) 56 .show(ui, |ui| { 57 let view = app.timelines[timeline].current_view(); 58 let len = view.notes.len(); 59 let mut bar_result: Option<BarResult> = None; 60 let txn = if let Ok(txn) = Transaction::new(&app.ndb) { 61 txn 62 } else { 63 warn!("failed to create transaction"); 64 return 0; 65 }; 66 67 view.list 68 .clone() 69 .borrow_mut() 70 .ui_custom_layout(ui, len, |ui, start_index| { 71 ui.spacing_mut().item_spacing.y = 0.0; 72 ui.spacing_mut().item_spacing.x = 4.0; 73 74 let ind = if reversed { 75 len - start_index - 1 76 } else { 77 start_index 78 }; 79 80 let note_key = app.timelines[timeline].current_view().notes[ind].key; 81 82 let note = if let Ok(note) = app.ndb.get_note_by_key(&txn, note_key) { 83 note 84 } else { 85 warn!("failed to query note {:?}", note_key); 86 return 0; 87 }; 88 89 ui::padding(8.0, ui, |ui| { 90 let textmode = app.textmode; 91 let resp = ui::NoteView::new(app, ¬e) 92 .note_previews(!textmode) 93 .selectable_text(false) 94 .show(ui); 95 96 if let Some(action) = resp.action { 97 let br = action.execute(app, timeline, note.id(), &txn); 98 if br.is_some() { 99 bar_result = br; 100 } 101 } else if resp.response.clicked() { 102 debug!("clicked note"); 103 } 104 }); 105 106 ui::hline(ui); 107 //ui.add(egui::Separator::default().spacing(0.0)); 108 109 1 110 }); 111 112 if let Some(br) = bar_result { 113 match br { 114 // update the thread for next render if we have new notes 115 BarResult::NewThreadNotes(new_notes) => { 116 let thread = app 117 .threads 118 .thread_mut(&app.ndb, &txn, new_notes.root_id.bytes()) 119 .get_ptr(); 120 new_notes.process(thread); 121 } 122 } 123 } 124 125 1 126 }); 127 } 128 129 fn postbox_view(app: &mut Damus, ui: &mut egui::Ui) { 130 // show a postbox in the first timeline 131 132 if let Some(account) = app.account_manager.get_selected_account_index() { 133 if app 134 .account_manager 135 .get_selected_account() 136 .map_or(false, |a| a.secret_key.is_some()) 137 { 138 if let Ok(txn) = Transaction::new(&app.ndb) { 139 let response = ui::PostView::new(app, DraftSource::Compose, account).ui(&txn, ui); 140 141 if let Some(action) = response.action { 142 match action { 143 PostAction::Post(np) => { 144 let seckey = app 145 .account_manager 146 .get_account(account) 147 .unwrap() 148 .secret_key 149 .as_ref() 150 .unwrap() 151 .to_secret_bytes(); 152 153 let note = np.to_note(&seckey); 154 let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); 155 info!("sending {}", raw_msg); 156 app.pool.send(&enostr::ClientMessage::raw(raw_msg)); 157 app.drafts.clear(DraftSource::Compose); 158 } 159 } 160 } 161 } 162 } 163 } 164 } 165 166 fn tabs_ui(ui: &mut egui::Ui) -> i32 { 167 ui.spacing_mut().item_spacing.y = 0.0; 168 169 let tab_res = egui_tabs::Tabs::new(2) 170 .selected(1) 171 .hover_bg(TabColor::none()) 172 .selected_fg(TabColor::none()) 173 .selected_bg(TabColor::none()) 174 .hover_bg(TabColor::none()) 175 //.hover_bg(TabColor::custom(egui::Color32::RED)) 176 .height(32.0) 177 .layout(Layout::centered_and_justified(Direction::TopDown)) 178 .show(ui, |ui, state| { 179 ui.spacing_mut().item_spacing.y = 0.0; 180 181 let ind = state.index(); 182 183 let txt = if ind == 0 { "Notes" } else { "Notes & Replies" }; 184 185 let res = ui.add(egui::Label::new(txt).selectable(false)); 186 187 // underline 188 if state.is_selected() { 189 let rect = res.rect; 190 let underline = 191 shrink_range_to_width(rect.x_range(), get_label_width(ui, txt) * 1.15); 192 let underline_y = ui.painter().round_to_pixel(rect.bottom()) - 1.5; 193 return (underline, underline_y); 194 } 195 196 (egui::Rangef::new(0.0, 0.0), 0.0) 197 }); 198 199 //ui.add_space(0.5); 200 ui::hline(ui); 201 202 let sel = tab_res.selected().unwrap_or_default(); 203 204 let (underline, underline_y) = tab_res.inner()[sel as usize].inner; 205 let underline_width = underline.span(); 206 207 let tab_anim_id = ui.id().with("tab_anim"); 208 let tab_anim_size = tab_anim_id.with("size"); 209 210 let stroke = egui::Stroke { 211 color: ui.visuals().hyperlink_color, 212 width: 2.0, 213 }; 214 215 let speed = 0.1f32; 216 217 // animate underline position 218 let x = ui 219 .ctx() 220 .animate_value_with_time(tab_anim_id, underline.min, speed); 221 222 // animate underline width 223 let w = ui 224 .ctx() 225 .animate_value_with_time(tab_anim_size, underline_width, speed); 226 227 let underline = egui::Rangef::new(x, x + w); 228 229 ui.painter().hline(underline, underline_y, stroke); 230 231 sel 232 } 233 234 fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 { 235 let font_id = egui::FontId::default(); 236 let galley = ui.fonts(|r| r.layout_no_wrap(text.to_string(), font_id, egui::Color32::WHITE)); 237 galley.rect.width() 238 } 239 240 fn shrink_range_to_width(range: egui::Rangef, width: f32) -> egui::Rangef { 241 let midpoint = (range.min + range.max) / 2.0; 242 let half_width = width / 2.0; 243 244 let min = midpoint - half_width; 245 let max = midpoint + half_width; 246 247 egui::Rangef::new(min, max) 248 }