mirror of
https://github.com/aljazceru/notedeck.git
synced 2026-01-16 23:04:19 +01:00
Also refactor damus app usage to only pass in things that we need in views. Signed-off-by: William Casarin <jb55@jb55.com>
299 lines
8.6 KiB
Rust
299 lines
8.6 KiB
Rust
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)
|
|
}
|