mirror of
https://github.com/aljazceru/notedeck.git
synced 2025-12-31 23:14:23 +01:00
330 lines
9.0 KiB
Rust
330 lines
9.0 KiB
Rust
use crate::notecache::CachedNote;
|
|
use crate::{ui, Damus};
|
|
|
|
use egui::containers::scroll_area::ScrollBarVisibility;
|
|
use egui::{Direction, Layout};
|
|
use egui_tabs::TabColor;
|
|
use egui_virtual_list::VirtualList;
|
|
use enostr::Filter;
|
|
use nostrdb::{Note, NoteKey, Subscription, Transaction};
|
|
use std::cell::RefCell;
|
|
use std::cmp::Ordering;
|
|
use std::rc::Rc;
|
|
|
|
use log::warn;
|
|
|
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
|
pub struct NoteRef {
|
|
pub key: NoteKey,
|
|
pub created_at: u64,
|
|
}
|
|
|
|
impl Ord for NoteRef {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
match self.created_at.cmp(&other.created_at) {
|
|
Ordering::Equal => self.key.cmp(&other.key),
|
|
Ordering::Less => Ordering::Greater,
|
|
Ordering::Greater => Ordering::Less,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for NoteRef {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
pub enum ViewFilter {
|
|
Notes,
|
|
NotesAndReplies,
|
|
}
|
|
|
|
impl ViewFilter {
|
|
pub fn name(&self) -> &'static str {
|
|
match self {
|
|
ViewFilter::Notes => "Notes",
|
|
ViewFilter::NotesAndReplies => "Notes & Replies",
|
|
}
|
|
}
|
|
|
|
pub fn index(&self) -> usize {
|
|
match self {
|
|
ViewFilter::Notes => 0,
|
|
ViewFilter::NotesAndReplies => 1,
|
|
}
|
|
}
|
|
|
|
pub fn filter_notes(cache: &CachedNote, note: &Note) -> bool {
|
|
!cache.reply.borrow(note.tags()).is_reply()
|
|
}
|
|
|
|
fn identity(_cache: &CachedNote, _note: &Note) -> bool {
|
|
true
|
|
}
|
|
|
|
pub fn filter(&self) -> fn(&CachedNote, &Note) -> bool {
|
|
match self {
|
|
ViewFilter::Notes => ViewFilter::filter_notes,
|
|
ViewFilter::NotesAndReplies => ViewFilter::identity,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A timeline view is a filtered view of notes in a timeline. Two standard views
|
|
/// are "Notes" and "Notes & Replies". A timeline is associated with a Filter,
|
|
/// but a TimelineView is a further filtered view of this Filter that can't
|
|
/// be captured by a Filter itself.
|
|
pub struct TimelineView {
|
|
pub notes: Vec<NoteRef>,
|
|
pub selection: i32,
|
|
pub filter: ViewFilter,
|
|
pub list: Rc<RefCell<VirtualList>>,
|
|
}
|
|
|
|
impl TimelineView {
|
|
pub fn new(filter: ViewFilter) -> Self {
|
|
let selection = 0i32;
|
|
let list = Rc::new(RefCell::new(VirtualList::new()));
|
|
let notes: Vec<NoteRef> = Vec::with_capacity(1000);
|
|
|
|
TimelineView {
|
|
notes,
|
|
selection,
|
|
filter,
|
|
list,
|
|
}
|
|
}
|
|
|
|
pub fn select_down(&mut self) {
|
|
if self.selection + 1 > self.notes.len() as i32 {
|
|
return;
|
|
}
|
|
|
|
self.selection += 1;
|
|
}
|
|
|
|
pub fn select_up(&mut self) {
|
|
if self.selection - 1 < 0 {
|
|
return;
|
|
}
|
|
|
|
self.selection -= 1;
|
|
}
|
|
}
|
|
|
|
pub struct Timeline {
|
|
pub filter: Vec<Filter>,
|
|
pub views: Vec<TimelineView>,
|
|
pub selected_view: i32,
|
|
|
|
/// Our nostrdb subscription
|
|
pub subscription: Option<Subscription>,
|
|
}
|
|
|
|
impl Timeline {
|
|
pub fn new(filter: Vec<Filter>) -> Self {
|
|
let subscription: Option<Subscription> = None;
|
|
let notes = TimelineView::new(ViewFilter::Notes);
|
|
let replies = TimelineView::new(ViewFilter::NotesAndReplies);
|
|
let views = vec![notes, replies];
|
|
let selected_view = 0;
|
|
|
|
Timeline {
|
|
filter,
|
|
views,
|
|
subscription,
|
|
selected_view,
|
|
}
|
|
}
|
|
|
|
pub fn current_view(&self) -> &TimelineView {
|
|
&self.views[self.selected_view as usize]
|
|
}
|
|
|
|
pub fn current_view_mut(&mut self) -> &mut TimelineView {
|
|
&mut self.views[self.selected_view as usize]
|
|
}
|
|
|
|
pub fn notes(&self, view: ViewFilter) -> &[NoteRef] {
|
|
&self.views[view.index()].notes
|
|
}
|
|
|
|
pub fn view(&self, view: ViewFilter) -> &TimelineView {
|
|
&self.views[view.index()]
|
|
}
|
|
|
|
pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineView {
|
|
&mut self.views[view.index()]
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
fn tabs_ui(timeline: &mut Timeline, ui: &mut egui::Ui) {
|
|
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 = if let Some(sel) = tab_res.selected() {
|
|
sel
|
|
} else {
|
|
0
|
|
};
|
|
|
|
// fun animation
|
|
timeline.selected_view = sel;
|
|
|
|
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: 3.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);
|
|
}
|
|
|
|
pub fn timeline_view(ui: &mut egui::Ui, app: &mut Damus, timeline: usize) {
|
|
//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;
|
|
*/
|
|
|
|
tabs_ui(&mut app.timelines[timeline], ui);
|
|
|
|
// need this for some reason??
|
|
ui.add_space(3.0);
|
|
|
|
let scroll_id = ui.id().with(app.timelines[timeline].selected_view);
|
|
egui::ScrollArea::vertical()
|
|
.id_source(scroll_id)
|
|
.animated(false)
|
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
|
|
.show(ui, |ui| {
|
|
let view = app.timelines[timeline].current_view();
|
|
let len = view.notes.len();
|
|
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 note_key = app.timelines[timeline].current_view().notes[start_index].key;
|
|
|
|
let txn = if let Ok(txn) = Transaction::new(&app.ndb) {
|
|
txn
|
|
} else {
|
|
warn!("failed to create transaction for {:?}", note_key);
|
|
return 0;
|
|
};
|
|
|
|
let note = if let Ok(note) = app.ndb.get_note_by_key(&txn, note_key) {
|
|
note
|
|
} else {
|
|
warn!("failed to query note {:?}", note_key);
|
|
return 0;
|
|
};
|
|
|
|
let textmode = app.textmode;
|
|
let note_ui = ui::Note::new(app, ¬e).note_previews(!textmode);
|
|
ui.add(note_ui);
|
|
ui::hline(ui);
|
|
//ui.add(egui::Separator::default().spacing(0.0));
|
|
|
|
1
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn merge_sorted_vecs<T: Ord + Copy>(vec1: &[T], vec2: &[T]) -> Vec<T> {
|
|
let mut merged = Vec::with_capacity(vec1.len() + vec2.len());
|
|
let mut i = 0;
|
|
let mut j = 0;
|
|
|
|
while i < vec1.len() && j < vec2.len() {
|
|
if vec1[i] <= vec2[j] {
|
|
merged.push(vec1[i]);
|
|
i += 1;
|
|
} else {
|
|
merged.push(vec2[j]);
|
|
j += 1;
|
|
}
|
|
}
|
|
|
|
// Append any remaining elements from either vector
|
|
if i < vec1.len() {
|
|
merged.extend_from_slice(&vec1[i..]);
|
|
}
|
|
if j < vec2.len() {
|
|
merged.extend_from_slice(&vec2[j..]);
|
|
}
|
|
|
|
merged
|
|
}
|