use crate::{ actionbar::BarAction, actionbar::BarResult, column::{Column, ColumnKind}, draft::Drafts, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui, ui::note::PostAction, }; use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; use enostr::{FilledKeypair, RelayPool}; use nostrdb::{Ndb, Note, Transaction}; use tracing::{debug, info, warn}; pub struct TimelineView<'a> { ndb: &'a Ndb, column: &'a mut Column, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, threads: &'a mut Threads, pool: &'a mut RelayPool, textmode: bool, reverse: bool, } impl<'a> TimelineView<'a> { pub fn new( ndb: &'a Ndb, column: &'a mut Column, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, threads: &'a mut Threads, pool: &'a mut RelayPool, textmode: bool, ) -> TimelineView<'a> { let reverse = false; TimelineView { ndb, column, note_cache, img_cache, threads, pool, reverse, textmode, } } pub fn ui(&mut self, ui: &mut egui::Ui) { timeline_ui( ui, self.ndb, self.column, self.note_cache, self.img_cache, self.threads, self.pool, self.reverse, self.textmode, ); } pub fn reversed(mut self) -> Self { self.reverse = true; self } } #[allow(clippy::too_many_arguments)] fn timeline_ui( ui: &mut egui::Ui, ndb: &Ndb, column: &mut Column, note_cache: &mut NoteCache, img_cache: &mut ImageCache, threads: &mut Threads, pool: &mut RelayPool, reversed: bool, textmode: bool, ) { //padding(4.0, ui, |ui| ui.heading("Notifications")); /* let font_id = egui::TextStyle::Body.resolve(ui.style()); let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y; */ { let timeline = if let ColumnKind::Timeline(timeline) = column.kind_mut() { timeline } else { return; }; timeline.selected_view = tabs_ui(ui); // need this for some reason?? ui.add_space(3.0); } let scroll_id = egui::Id::new(("tlscroll", column.view_id())); egui::ScrollArea::vertical() .id_source(scroll_id) .animated(false) .auto_shrink([false, false]) .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) .show(ui, |ui| { let timeline = if let ColumnKind::Timeline(timeline) = column.kind_mut() { timeline } else { return 0; }; let view = timeline.current_view(); let len = view.notes.len(); let txn = if let Ok(txn) = Transaction::new(ndb) { txn } else { warn!("failed to create transaction"); return 0; }; let mut bar_action: Option<(BarAction, Note)> = None; view.list .clone() .borrow_mut() .ui_custom_layout(ui, len, |ui, start_index| { ui.spacing_mut().item_spacing.y = 0.0; ui.spacing_mut().item_spacing.x = 4.0; let ind = if reversed { len - start_index - 1 } else { start_index }; let note_key = timeline.current_view().notes[ind].key; let note = if let Ok(note) = ndb.get_note_by_key(&txn, note_key) { note } else { warn!("failed to query note {:?}", note_key); return 0; }; ui::padding(8.0, ui, |ui| { let resp = ui::NoteView::new(ndb, note_cache, img_cache, ¬e) .note_previews(!textmode) .selectable_text(false) .show(ui); if let Some(ba) = resp.action { bar_action = Some((ba, note)); } else if resp.response.clicked() { debug!("clicked note"); } }); ui::hline(ui); //ui.add(egui::Separator::default().spacing(0.0)); 1 }); // handle any actions from the virtual list if let Some((action, note)) = bar_action { if let Some(br) = action.execute(ndb, column, threads, note_cache, pool, note.id(), &txn) { match br { // update the thread for next render if we have new notes BarResult::NewThreadNotes(new_notes) => { let thread = threads .thread_mut(ndb, &txn, new_notes.root_id.bytes()) .get_ptr(); new_notes.process(thread); } } } } 1 }); } pub fn postbox_view<'a>( ndb: &'a Ndb, key: FilledKeypair<'a>, pool: &'a mut RelayPool, drafts: &'a mut Drafts, img_cache: &'a mut ImageCache, ui: &'a mut egui::Ui, ) { // show a postbox in the first timeline let txn = Transaction::new(ndb).expect("txn"); let response = ui::PostView::new(ndb, drafts.compose_mut(), img_cache, key).ui(&txn, ui); if let Some(action) = response.action { match action { PostAction::Post(np) => { let seckey = key.secret_key.to_secret_bytes(); let note = np.to_note(&seckey); let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); info!("sending {}", raw_msg); pool.send(&enostr::ClientMessage::raw(raw_msg)); drafts.compose_mut().clear(); } } } } fn tabs_ui(ui: &mut egui::Ui) -> i32 { ui.spacing_mut().item_spacing.y = 0.0; let tab_res = egui_tabs::Tabs::new(2) .selected(1) .hover_bg(TabColor::none()) .selected_fg(TabColor::none()) .selected_bg(TabColor::none()) .hover_bg(TabColor::none()) //.hover_bg(TabColor::custom(egui::Color32::RED)) .height(32.0) .layout(Layout::centered_and_justified(Direction::TopDown)) .show(ui, |ui, state| { ui.spacing_mut().item_spacing.y = 0.0; let ind = state.index(); let txt = if ind == 0 { "Notes" } else { "Notes & Replies" }; let res = ui.add(egui::Label::new(txt).selectable(false)); // underline if state.is_selected() { let rect = res.rect; let underline = shrink_range_to_width(rect.x_range(), get_label_width(ui, txt) * 1.15); let underline_y = ui.painter().round_to_pixel(rect.bottom()) - 1.5; return (underline, underline_y); } (egui::Rangef::new(0.0, 0.0), 0.0) }); //ui.add_space(0.5); ui::hline(ui); let sel = tab_res.selected().unwrap_or_default(); let (underline, underline_y) = tab_res.inner()[sel as usize].inner; let underline_width = underline.span(); let tab_anim_id = ui.id().with("tab_anim"); let tab_anim_size = tab_anim_id.with("size"); let stroke = egui::Stroke { color: ui.visuals().hyperlink_color, width: 2.0, }; let speed = 0.1f32; // animate underline position let x = ui .ctx() .animate_value_with_time(tab_anim_id, underline.min, speed); // animate underline width let w = ui .ctx() .animate_value_with_time(tab_anim_size, underline_width, speed); let underline = egui::Rangef::new(x, x + w); ui.painter().hline(underline, underline_y, stroke); sel } fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 { let font_id = egui::FontId::default(); let galley = ui.fonts(|r| r.layout_no_wrap(text.to_string(), font_id, egui::Color32::WHITE)); galley.rect.width() } fn shrink_range_to_width(range: egui::Rangef, width: f32) -> egui::Rangef { let midpoint = (range.min + range.max) / 2.0; let half_width = width / 2.0; let min = midpoint - half_width; let max = midpoint + half_width; egui::Rangef::new(min, max) }