timeline: refactor tabs into TimelineView
TimelineView is a filtered view of a timeline. We will use this for future tab rendering. We also introduce a new "selection" concept for selecting notes on different timeline views. This is in preparation for vim keybindings.
This commit is contained in:
57
src/app.rs
57
src/app.rs
@@ -3,7 +3,7 @@ use crate::app_style::user_requested_visuals_change;
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::frame_history::FrameHistory;
|
use crate::frame_history::FrameHistory;
|
||||||
use crate::imgcache::ImageCache;
|
use crate::imgcache::ImageCache;
|
||||||
use crate::notecache::NoteCache;
|
use crate::notecache::{CachedNote, NoteCache};
|
||||||
use crate::timeline;
|
use crate::timeline;
|
||||||
use crate::timeline::{NoteRef, Timeline};
|
use crate::timeline::{NoteRef, Timeline};
|
||||||
use crate::ui::is_mobile;
|
use crate::ui::is_mobile;
|
||||||
@@ -15,7 +15,7 @@ use egui_extras::{Size, StripBuilder};
|
|||||||
use enostr::{ClientMessage, Filter, Pubkey, RelayEvent, RelayMessage};
|
use enostr::{ClientMessage, Filter, Pubkey, RelayEvent, RelayMessage};
|
||||||
use nostrdb::{BlockType, Config, Mention, Ndb, Note, NoteKey, Transaction};
|
use nostrdb::{BlockType, Config, Mention, Ndb, Note, NoteKey, Transaction};
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashSet;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -33,11 +33,13 @@ pub enum DamusState {
|
|||||||
pub struct Damus {
|
pub struct Damus {
|
||||||
state: DamusState,
|
state: DamusState,
|
||||||
//compose: String,
|
//compose: String,
|
||||||
note_cache: HashMap<NoteKey, NoteCache>,
|
note_cache: NoteCache,
|
||||||
pool: RelayPool,
|
pool: RelayPool,
|
||||||
|
|
||||||
pub textmode: bool,
|
pub textmode: bool,
|
||||||
|
|
||||||
pub timelines: Vec<Timeline>,
|
pub timelines: Vec<Timeline>,
|
||||||
|
pub selected_timeline: i32,
|
||||||
|
|
||||||
pub img_cache: ImageCache,
|
pub img_cache: ImageCache,
|
||||||
pub ndb: Ndb,
|
pub ndb: Ndb,
|
||||||
@@ -94,7 +96,7 @@ fn send_initial_filters(damus: &mut Damus, relay_url: &str) {
|
|||||||
for timeline in &damus.timelines {
|
for timeline in &damus.timelines {
|
||||||
let mut filter = timeline.filter.clone();
|
let mut filter = timeline.filter.clone();
|
||||||
for f in &mut filter {
|
for f in &mut filter {
|
||||||
since_optimize_filter(f, &timeline.notes);
|
since_optimize_filter(f, timeline.notes());
|
||||||
}
|
}
|
||||||
relay.subscribe(format!("initial{}", c), filter);
|
relay.subscribe(format!("initial{}", c), filter);
|
||||||
c += 1;
|
c += 1;
|
||||||
@@ -298,13 +300,14 @@ fn poll_notes_for_timeline<'a>(
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let timeline = &mut damus.timelines[timeline];
|
let timeline = &mut damus.timelines[timeline];
|
||||||
let prev_items = timeline.notes.len();
|
let prev_items = timeline.notes().len();
|
||||||
timeline.notes = timeline::merge_sorted_vecs(&timeline.notes, &new_refs);
|
timeline.current_view_mut().notes = timeline::merge_sorted_vecs(&timeline.notes(), &new_refs);
|
||||||
let new_items = timeline.notes.len() - prev_items;
|
let new_items = timeline.notes().len() - prev_items;
|
||||||
|
|
||||||
// TODO: technically items could have been added inbetween
|
// TODO: technically items could have been added inbetween
|
||||||
if new_items > 0 {
|
if new_items > 0 {
|
||||||
timeline
|
timeline
|
||||||
|
.current_view()
|
||||||
.list
|
.list
|
||||||
.clone()
|
.clone()
|
||||||
.lock()
|
.lock()
|
||||||
@@ -339,7 +342,7 @@ fn setup_initial_nostrdb_subs(damus: &mut Damus) -> Result<()> {
|
|||||||
filters,
|
filters,
|
||||||
timeline.filter[0].limit.unwrap_or(200) as i32,
|
timeline.filter[0].limit.unwrap_or(200) as i32,
|
||||||
)?;
|
)?;
|
||||||
timeline.notes = res
|
timeline.notes_view_mut().notes = res
|
||||||
.iter()
|
.iter()
|
||||||
.map(|qr| NoteRef {
|
.map(|qr| NoteRef {
|
||||||
key: qr.note_key,
|
key: qr.note_key,
|
||||||
@@ -384,7 +387,7 @@ fn get_unknown_ids<'a>(txn: &'a Transaction, damus: &Damus) -> Result<Vec<Unknow
|
|||||||
let mut ids: HashSet<UnknownId> = HashSet::new();
|
let mut ids: HashSet<UnknownId> = HashSet::new();
|
||||||
|
|
||||||
for timeline in &damus.timelines {
|
for timeline in &damus.timelines {
|
||||||
for noteref in &timeline.notes {
|
for noteref in timeline.notes() {
|
||||||
let note = damus.ndb.get_note_by_key(txn, noteref.key)?;
|
let note = damus.ndb.get_note_by_key(txn, noteref.key)?;
|
||||||
let _ = get_unknown_note_ids(&damus.ndb, txn, ¬e, note.key().unwrap(), &mut ids);
|
let _ = get_unknown_note_ids(&damus.ndb, txn, ¬e, note.key().unwrap(), &mut ids);
|
||||||
}
|
}
|
||||||
@@ -506,7 +509,8 @@ impl Damus {
|
|||||||
state: DamusState::Initializing,
|
state: DamusState::Initializing,
|
||||||
pool: RelayPool::new(),
|
pool: RelayPool::new(),
|
||||||
img_cache: ImageCache::new(imgcache_dir),
|
img_cache: ImageCache::new(imgcache_dir),
|
||||||
note_cache: HashMap::new(),
|
note_cache: NoteCache::default(),
|
||||||
|
selected_timeline: 0,
|
||||||
timelines,
|
timelines,
|
||||||
textmode: false,
|
textmode: false,
|
||||||
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
|
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
|
||||||
@@ -515,10 +519,37 @@ impl Damus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_note_cache_mut(&mut self, note_key: NoteKey, note: &Note<'_>) -> &mut NoteCache {
|
pub fn get_note_cache_mut(&mut self, note_key: NoteKey, note: &Note<'_>) -> &mut CachedNote {
|
||||||
self.note_cache
|
self.note_cache
|
||||||
|
.cache
|
||||||
.entry(note_key)
|
.entry(note_key)
|
||||||
.or_insert_with(|| NoteCache::new(note))
|
.or_insert_with(|| CachedNote::new(note))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_timeline(&mut self) -> &mut Timeline {
|
||||||
|
&mut self.timelines[self.selected_timeline as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_down(&mut self) {
|
||||||
|
self.selected_timeline().current_view_mut().select_down();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_up(&mut self) {
|
||||||
|
self.selected_timeline().current_view_mut().select_up();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_left(&mut self) {
|
||||||
|
if self.selected_timeline - 1 < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.selected_timeline -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_right(&mut self) {
|
||||||
|
if self.selected_timeline + 1 >= self.timelines.len() as i32 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.selected_timeline += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,7 +629,7 @@ fn render_panel(ctx: &egui::Context, app: &mut Damus, timeline_ind: usize) {
|
|||||||
|
|
||||||
ui.weak(format!(
|
ui.weak(format!(
|
||||||
"{} notes",
|
"{} notes",
|
||||||
&app.timelines[timeline_ind].notes.len()
|
&app.timelines[timeline_ind].notes().len()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
use crate::time::time_ago_since;
|
use crate::time::time_ago_since;
|
||||||
use crate::timecache::TimeCached;
|
use crate::timecache::TimeCached;
|
||||||
use nostrdb::{Note, NoteReply, NoteReplyBuf};
|
use nostrdb::{Note, NoteKey, NoteReply, NoteReplyBuf};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct NoteCache {
|
pub struct NoteCache {
|
||||||
|
pub cache: HashMap<NoteKey, CachedNote>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CachedNote {
|
||||||
reltime: TimeCached<String>,
|
reltime: TimeCached<String>,
|
||||||
pub reply: NoteReplyBuf,
|
pub reply: NoteReplyBuf,
|
||||||
pub bar_open: bool,
|
pub bar_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NoteCache {
|
impl CachedNote {
|
||||||
pub fn new(note: &Note<'_>) -> Self {
|
pub fn new(note: &Note<'_>) -> Self {
|
||||||
let created_at = note.created_at();
|
let created_at = note.created_at();
|
||||||
let reltime = TimeCached::new(
|
let reltime = TimeCached::new(
|
||||||
@@ -18,7 +24,7 @@ impl NoteCache {
|
|||||||
);
|
);
|
||||||
let reply = NoteReply::new(note.tags()).to_owned();
|
let reply = NoteReply::new(note.tags()).to_owned();
|
||||||
let bar_open = false;
|
let bar_open = false;
|
||||||
NoteCache {
|
CachedNote {
|
||||||
reltime,
|
reltime,
|
||||||
reply,
|
reply,
|
||||||
bar_open,
|
bar_open,
|
||||||
|
|||||||
113
src/timeline.rs
113
src/timeline.rs
@@ -1,10 +1,12 @@
|
|||||||
|
use crate::notecache::CachedNote;
|
||||||
use crate::{ui, Damus};
|
use crate::{ui, Damus};
|
||||||
|
|
||||||
use egui::containers::scroll_area::ScrollBarVisibility;
|
use egui::containers::scroll_area::ScrollBarVisibility;
|
||||||
use egui::{Direction, Layout};
|
use egui::{Direction, Layout};
|
||||||
use egui_tabs::TabColor;
|
use egui_tabs::TabColor;
|
||||||
use egui_virtual_list::VirtualList;
|
use egui_virtual_list::VirtualList;
|
||||||
use enostr::Filter;
|
use enostr::Filter;
|
||||||
use nostrdb::{NoteKey, Subscription, Transaction};
|
use nostrdb::{Note, NoteKey, Subscription, Transaction};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
@@ -32,30 +34,116 @@ impl PartialOrd for NoteRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum ViewFilter {
|
||||||
|
Notes,
|
||||||
|
NotesAndReplies,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewFilter {
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ViewFilter::Notes => "Notes",
|
||||||
|
ViewFilter::NotesAndReplies => "Notes & Replies",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
ViewFilter::Notes => 0,
|
||||||
|
ViewFilter::NotesAndReplies => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter(&self, cache: &CachedNote, note: &Note) -> bool {
|
||||||
|
match self {
|
||||||
|
ViewFilter::Notes => !cache.reply.borrow(note.tags()).is_reply(),
|
||||||
|
ViewFilter::NotesAndReplies => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: Arc<Mutex<VirtualList>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimelineView {
|
||||||
|
pub fn new(filter: ViewFilter) -> Self {
|
||||||
|
let selection = 0i32;
|
||||||
|
let list = Arc::new(Mutex::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 struct Timeline {
|
||||||
pub filter: Vec<Filter>,
|
pub filter: Vec<Filter>,
|
||||||
pub notes: Vec<NoteRef>,
|
pub views: Vec<TimelineView>,
|
||||||
|
pub selected_view: i32,
|
||||||
|
|
||||||
/// Our nostrdb subscription
|
/// Our nostrdb subscription
|
||||||
pub subscription: Option<Subscription>,
|
pub subscription: Option<Subscription>,
|
||||||
|
|
||||||
/// State for our virtual list egui widget
|
|
||||||
pub list: Arc<Mutex<VirtualList>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timeline {
|
impl Timeline {
|
||||||
pub fn new(filter: Vec<Filter>) -> Self {
|
pub fn new(filter: Vec<Filter>) -> Self {
|
||||||
let notes: Vec<NoteRef> = Vec::with_capacity(1000);
|
|
||||||
let subscription: Option<Subscription> = None;
|
let subscription: Option<Subscription> = None;
|
||||||
let list = Arc::new(Mutex::new(VirtualList::new()));
|
let notes = TimelineView::new(ViewFilter::Notes);
|
||||||
|
let replies = TimelineView::new(ViewFilter::NotesAndReplies);
|
||||||
|
let views = vec![notes, replies];
|
||||||
|
let selected_view = 0;
|
||||||
|
|
||||||
Timeline {
|
Timeline {
|
||||||
filter,
|
filter,
|
||||||
notes,
|
views,
|
||||||
subscription,
|
subscription,
|
||||||
list,
|
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) -> &[NoteRef] {
|
||||||
|
&self.views[ViewFilter::NotesAndReplies.index()].notes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notes_view_mut(&mut self) -> &mut TimelineView {
|
||||||
|
&mut self.views[ViewFilter::NotesAndReplies.index()]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 {
|
fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 {
|
||||||
@@ -156,15 +244,16 @@ pub fn timeline_view(ui: &mut egui::Ui, app: &mut Damus, timeline: usize) {
|
|||||||
.animated(false)
|
.animated(false)
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let len = app.timelines[timeline].notes.len();
|
let view = app.timelines[timeline].current_view();
|
||||||
let list = app.timelines[timeline].list.clone();
|
let len = view.notes.len();
|
||||||
|
let list = view.list.clone();
|
||||||
list.lock()
|
list.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.ui_custom_layout(ui, len, |ui, start_index| {
|
.ui_custom_layout(ui, len, |ui, start_index| {
|
||||||
ui.spacing_mut().item_spacing.y = 0.0;
|
ui.spacing_mut().item_spacing.y = 0.0;
|
||||||
ui.spacing_mut().item_spacing.x = 4.0;
|
ui.spacing_mut().item_spacing.x = 4.0;
|
||||||
|
|
||||||
let note_key = app.timelines[timeline].notes[start_index].key;
|
let note_key = app.timelines[timeline].current_view().notes[start_index].key;
|
||||||
|
|
||||||
let txn = if let Ok(txn) = Transaction::new(&app.ndb) {
|
let txn = if let Ok(txn) = Transaction::new(&app.ndb) {
|
||||||
txn
|
txn
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ pub mod options;
|
|||||||
pub use contents::NoteContents;
|
pub use contents::NoteContents;
|
||||||
pub use options::NoteOptions;
|
pub use options::NoteOptions;
|
||||||
|
|
||||||
use crate::{colors, ui, ui::is_mobile, Damus};
|
use crate::{colors, notecache::CachedNote, ui, ui::is_mobile, Damus};
|
||||||
use egui::{Label, RichText, Sense};
|
use egui::{Label, RichText, Sense};
|
||||||
use nostrdb::{NoteKey, Transaction};
|
use nostrdb::{NoteKey, Transaction};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
@@ -308,7 +308,7 @@ fn secondary_label(ui: &mut egui::Ui, s: impl Into<String>) {
|
|||||||
|
|
||||||
fn render_reltime(
|
fn render_reltime(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
note_cache: &mut crate::notecache::NoteCache,
|
note_cache: &mut CachedNote,
|
||||||
before: bool,
|
before: bool,
|
||||||
) -> egui::InnerResponse<()> {
|
) -> egui::InnerResponse<()> {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
|
|||||||
Reference in New Issue
Block a user