ui: move timeline view to its own file
Also add some thread methods for fetching new notes Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
30
src/app.rs
30
src/app.rs
@@ -10,7 +10,6 @@ use crate::notecache::{CachedNote, NoteCache};
|
|||||||
use crate::relay_pool_manager::RelayPoolManager;
|
use crate::relay_pool_manager::RelayPoolManager;
|
||||||
use crate::route::Route;
|
use crate::route::Route;
|
||||||
use crate::thread::{DecrementResult, Threads};
|
use crate::thread::{DecrementResult, Threads};
|
||||||
use crate::timeline;
|
|
||||||
use crate::timeline::{Timeline, TimelineSource, ViewFilter};
|
use crate::timeline::{Timeline, TimelineSource, ViewFilter};
|
||||||
use crate::ui::note::PostAction;
|
use crate::ui::note::PostAction;
|
||||||
use crate::ui::{self, AccountSelectionWidget, DesktopGlobalPopup};
|
use crate::ui::{self, AccountSelectionWidget, DesktopGlobalPopup};
|
||||||
@@ -94,27 +93,6 @@ fn relay_setup(pool: &mut RelayPool, ctx: &egui::Context) {
|
|||||||
/// notes locally. One way to determine this is by looking at the current filter
|
/// notes locally. One way to determine this is by looking at the current filter
|
||||||
/// and seeing what its limit is. If we have less notes than the limit,
|
/// and seeing what its limit is. If we have less notes than the limit,
|
||||||
/// we might want to backfill older notes
|
/// we might want to backfill older notes
|
||||||
fn should_since_optimize(limit: Option<u16>, num_notes: usize) -> bool {
|
|
||||||
let limit = limit.unwrap_or(enostr::Filter::default_limit()) as usize;
|
|
||||||
|
|
||||||
// rough heuristic for bailing since optimization if we don't have enough notes
|
|
||||||
limit <= num_notes
|
|
||||||
}
|
|
||||||
|
|
||||||
fn since_optimize_filter(filter: &mut enostr::Filter, notes: &[NoteRef]) {
|
|
||||||
// Get the latest entry in the events
|
|
||||||
if notes.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the latest note
|
|
||||||
let latest = notes[0];
|
|
||||||
let since = latest.created_at - 60;
|
|
||||||
|
|
||||||
// update the filters
|
|
||||||
filter.since = Some(since);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_initial_filters(damus: &mut Damus, relay_url: &str) {
|
fn send_initial_filters(damus: &mut Damus, relay_url: &str) {
|
||||||
info!("Sending initial filters to {}", relay_url);
|
info!("Sending initial filters to {}", relay_url);
|
||||||
let mut c: u32 = 1;
|
let mut c: u32 = 1;
|
||||||
@@ -133,8 +111,8 @@ fn send_initial_filters(damus: &mut Damus, relay_url: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let notes = timeline.notes(ViewFilter::NotesAndReplies);
|
let notes = timeline.notes(ViewFilter::NotesAndReplies);
|
||||||
if should_since_optimize(f.limit, notes.len()) {
|
if crate::filter::should_since_optimize(f.limit, notes.len()) {
|
||||||
since_optimize_filter(f, notes);
|
crate::filter::since_optimize_filter(f, notes);
|
||||||
} else {
|
} else {
|
||||||
warn!("Skipping since optimization for {:?}: number of local notes is less than limit, attempting to backfill.", f);
|
warn!("Skipping since optimization for {:?}: number of local notes is less than limit, attempting to backfill.", f);
|
||||||
}
|
}
|
||||||
@@ -650,6 +628,7 @@ fn parse_args(args: &[String]) -> Args {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
fn determine_key_storage_type() -> KeyStorageType {
|
fn determine_key_storage_type() -> KeyStorageType {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
@@ -666,6 +645,7 @@ fn determine_key_storage_type() -> KeyStorageType {
|
|||||||
KeyStorageType::None
|
KeyStorageType::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
impl Damus {
|
impl Damus {
|
||||||
/// Called once before the first frame.
|
/// Called once before the first frame.
|
||||||
@@ -955,7 +935,7 @@ fn render_nav(routes: Vec<Route>, timeline_ind: usize, app: &mut Damus, ui: &mut
|
|||||||
.show(ui, |ui, nav| match nav.top() {
|
.show(ui, |ui, nav| match nav.top() {
|
||||||
Route::Timeline(_n) => {
|
Route::Timeline(_n) => {
|
||||||
let app = &mut app_ctx.borrow_mut();
|
let app = &mut app_ctx.borrow_mut();
|
||||||
timeline::timeline_view(ui, app, timeline_ind);
|
ui::TimelineView::new(app, timeline_ind).ui(ui);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,30 @@
|
|||||||
|
use crate::note::NoteRef;
|
||||||
|
|
||||||
|
pub fn should_since_optimize(limit: Option<u16>, num_notes: usize) -> bool {
|
||||||
|
let limit = limit.unwrap_or(enostr::Filter::default_limit()) as usize;
|
||||||
|
|
||||||
|
// rough heuristic for bailing since optimization if we don't have enough notes
|
||||||
|
limit <= num_notes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn since_optimize_filter_with(filter: &mut enostr::Filter, notes: &[NoteRef], since_gap: u64) {
|
||||||
|
// Get the latest entry in the events
|
||||||
|
if notes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the latest note
|
||||||
|
let latest = notes[0];
|
||||||
|
let since = latest.created_at - since_gap;
|
||||||
|
|
||||||
|
// update the filters
|
||||||
|
filter.since = Some(since);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn since_optimize_filter(filter: &mut enostr::Filter, notes: &[NoteRef]) {
|
||||||
|
since_optimize_filter_with(filter, notes, 60);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn convert_enostr_filter(filter: &enostr::Filter) -> nostrdb::Filter {
|
pub fn convert_enostr_filter(filter: &enostr::Filter) -> nostrdb::Filter {
|
||||||
let mut nfilter = nostrdb::Filter::new();
|
let mut nfilter = nostrdb::Filter::new();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::note::NoteRef;
|
use crate::note::NoteRef;
|
||||||
use crate::timeline::{TimelineView, ViewFilter};
|
use crate::timeline::{TimelineTab, ViewFilter};
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use nostrdb::{Filter, Ndb, Subscription, Transaction};
|
use nostrdb::{Filter, Ndb, Subscription, Transaction};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -7,7 +7,7 @@ use tracing::debug;
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Thread {
|
pub struct Thread {
|
||||||
pub view: TimelineView,
|
pub view: TimelineTab,
|
||||||
sub: Option<Subscription>,
|
sub: Option<Subscription>,
|
||||||
pub subscribers: i32,
|
pub subscribers: i32,
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ impl Thread {
|
|||||||
if cap == 0 {
|
if cap == 0 {
|
||||||
cap = 25;
|
cap = 25;
|
||||||
}
|
}
|
||||||
let mut view = TimelineView::new_with_capacity(ViewFilter::NotesAndReplies, cap);
|
let mut view = TimelineTab::new_with_capacity(ViewFilter::NotesAndReplies, cap);
|
||||||
view.notes = notes;
|
view.notes = notes;
|
||||||
let sub: Option<Subscription> = None;
|
let sub: Option<Subscription> = None;
|
||||||
let subscribers: i32 = 0;
|
let subscribers: i32 = 0;
|
||||||
@@ -36,6 +36,34 @@ impl Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Look for new thread notes since our last fetch
|
||||||
|
pub fn new_notes(
|
||||||
|
notes: &[NoteRef],
|
||||||
|
root_id: &[u8; 32],
|
||||||
|
txn: &Transaction,
|
||||||
|
ndb: &Ndb,
|
||||||
|
) -> Vec<NoteRef> {
|
||||||
|
if notes.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_note = notes[0];
|
||||||
|
let filters = Thread::filters_since(root_id, last_note.created_at - 60);
|
||||||
|
|
||||||
|
if let Ok(results) = ndb.query(txn, filters, 1000) {
|
||||||
|
results
|
||||||
|
.into_iter()
|
||||||
|
.map(NoteRef::from_query_result)
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"got no results from thread update for {}",
|
||||||
|
hex::encode(root_id)
|
||||||
|
);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn decrement_sub(&mut self) -> Result<DecrementResult, Error> {
|
pub fn decrement_sub(&mut self) -> Result<DecrementResult, Error> {
|
||||||
debug!("decrementing sub {:?}", self.subscription().map(|s| s.id));
|
debug!("decrementing sub {:?}", self.subscription().map(|s| s.id));
|
||||||
self.subscribers -= 1;
|
self.subscribers -= 1;
|
||||||
@@ -60,9 +88,22 @@ impl Thread {
|
|||||||
pub fn subscription_mut(&mut self) -> &mut Option<Subscription> {
|
pub fn subscription_mut(&mut self) -> &mut Option<Subscription> {
|
||||||
&mut self.sub
|
&mut self.sub
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Thread {
|
pub fn filters_since(root: &[u8; 32], since: u64) -> Vec<Filter> {
|
||||||
|
vec![
|
||||||
|
nostrdb::Filter::new()
|
||||||
|
.since(since)
|
||||||
|
.kinds(vec![1])
|
||||||
|
.event(root)
|
||||||
|
.build(),
|
||||||
|
nostrdb::Filter::new()
|
||||||
|
.kinds(vec![1])
|
||||||
|
.ids(vec![*root])
|
||||||
|
.since(since)
|
||||||
|
.build(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn filters(root: &[u8; 32]) -> Vec<Filter> {
|
pub fn filters(root: &[u8; 32]) -> Vec<Filter> {
|
||||||
vec![
|
vec![
|
||||||
nostrdb::Filter::new().kinds(vec![1]).event(root).build(),
|
nostrdb::Filter::new().kinds(vec![1]).event(root).build(),
|
||||||
@@ -106,7 +147,7 @@ impl Threads {
|
|||||||
let root = if let Ok(root) = ndb.get_note_by_id(txn, root_id) {
|
let root = if let Ok(root) = ndb.get_note_by_id(txn, root_id) {
|
||||||
root
|
root
|
||||||
} else {
|
} else {
|
||||||
debug!("couldnt find root note for id {}", hex::encode(root_id));
|
debug!("couldnt find root note root_id:{}", hex::encode(root_id));
|
||||||
self.root_id_to_thread
|
self.root_id_to_thread
|
||||||
.insert(root_id.to_owned(), Thread::new(vec![]));
|
.insert(root_id.to_owned(), Thread::new(vec![]));
|
||||||
return self.root_id_to_thread.get_mut(root_id).unwrap();
|
return self.root_id_to_thread.get_mut(root_id).unwrap();
|
||||||
@@ -115,9 +156,7 @@ impl Threads {
|
|||||||
// we don't have the thread, query for it!
|
// we don't have the thread, query for it!
|
||||||
let filters = Thread::filters(root_id);
|
let filters = Thread::filters(root_id);
|
||||||
|
|
||||||
// TODO: what should be the max results ?
|
let notes = if let Ok(results) = ndb.query(txn, filters, 1000) {
|
||||||
let notes = if let Ok(mut results) = ndb.query(txn, filters, 10000) {
|
|
||||||
results.reverse();
|
|
||||||
results
|
results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(NoteRef::from_query_result)
|
.map(NoteRef::from_query_result)
|
||||||
|
|||||||
222
src/timeline.rs
222
src/timeline.rs
@@ -1,16 +1,11 @@
|
|||||||
use crate::app::{get_unknown_note_ids, UnknownId};
|
use crate::app::{get_unknown_note_ids, UnknownId};
|
||||||
use crate::draft::DraftSource;
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::note::NoteRef;
|
use crate::note::NoteRef;
|
||||||
use crate::notecache::CachedNote;
|
use crate::notecache::CachedNote;
|
||||||
use crate::ui::note::PostAction;
|
use crate::{Damus, Result};
|
||||||
use crate::{ui, Damus, Result};
|
|
||||||
|
|
||||||
use crate::route::Route;
|
use crate::route::Route;
|
||||||
use egui::containers::scroll_area::ScrollBarVisibility;
|
|
||||||
use egui::{Direction, Layout};
|
|
||||||
|
|
||||||
use egui_tabs::TabColor;
|
|
||||||
use egui_virtual_list::VirtualList;
|
use egui_virtual_list::VirtualList;
|
||||||
use enostr::Filter;
|
use enostr::Filter;
|
||||||
use nostrdb::{Note, Subscription, Transaction};
|
use nostrdb::{Note, Subscription, Transaction};
|
||||||
@@ -18,7 +13,7 @@ use std::cell::RefCell;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use tracing::{debug, info, warn};
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum TimelineSource<'a> {
|
pub enum TimelineSource<'a> {
|
||||||
@@ -36,7 +31,7 @@ impl<'a> TimelineSource<'a> {
|
|||||||
app: &'b mut Damus,
|
app: &'b mut Damus,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
filter: ViewFilter,
|
filter: ViewFilter,
|
||||||
) -> &'b mut TimelineView {
|
) -> &'b mut TimelineTab {
|
||||||
match self {
|
match self {
|
||||||
TimelineSource::Column { ind, .. } => app.timelines[ind].view_mut(filter),
|
TimelineSource::Column { ind, .. } => app.timelines[ind].view_mut(filter),
|
||||||
TimelineSource::Thread(root_id) => {
|
TimelineSource::Thread(root_id) => {
|
||||||
@@ -187,19 +182,19 @@ impl ViewFilter {
|
|||||||
|
|
||||||
/// A timeline view is a filtered view of notes in a timeline. Two standard views
|
/// 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,
|
/// 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
|
/// but a TimelineTab is a further filtered view of this Filter that can't
|
||||||
/// be captured by a Filter itself.
|
/// be captured by a Filter itself.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct TimelineView {
|
pub struct TimelineTab {
|
||||||
pub notes: Vec<NoteRef>,
|
pub notes: Vec<NoteRef>,
|
||||||
pub selection: i32,
|
pub selection: i32,
|
||||||
pub filter: ViewFilter,
|
pub filter: ViewFilter,
|
||||||
pub list: Rc<RefCell<VirtualList>>,
|
pub list: Rc<RefCell<VirtualList>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimelineView {
|
impl TimelineTab {
|
||||||
pub fn new(filter: ViewFilter) -> Self {
|
pub fn new(filter: ViewFilter) -> Self {
|
||||||
TimelineView::new_with_capacity(filter, 1000)
|
TimelineTab::new_with_capacity(filter, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_capacity(filter: ViewFilter, cap: usize) -> Self {
|
pub fn new_with_capacity(filter: ViewFilter, cap: usize) -> Self {
|
||||||
@@ -209,7 +204,7 @@ impl TimelineView {
|
|||||||
let list = Rc::new(RefCell::new(list));
|
let list = Rc::new(RefCell::new(list));
|
||||||
let notes: Vec<NoteRef> = Vec::with_capacity(cap);
|
let notes: Vec<NoteRef> = Vec::with_capacity(cap);
|
||||||
|
|
||||||
TimelineView {
|
TimelineTab {
|
||||||
notes,
|
notes,
|
||||||
selection,
|
selection,
|
||||||
filter,
|
filter,
|
||||||
@@ -257,7 +252,7 @@ impl TimelineView {
|
|||||||
|
|
||||||
pub struct Timeline {
|
pub struct Timeline {
|
||||||
pub filter: Vec<Filter>,
|
pub filter: Vec<Filter>,
|
||||||
pub views: Vec<TimelineView>,
|
pub views: Vec<TimelineTab>,
|
||||||
pub selected_view: i32,
|
pub selected_view: i32,
|
||||||
pub routes: Vec<Route>,
|
pub routes: Vec<Route>,
|
||||||
pub navigating: bool,
|
pub navigating: bool,
|
||||||
@@ -270,8 +265,8 @@ pub struct Timeline {
|
|||||||
impl Timeline {
|
impl Timeline {
|
||||||
pub fn new(filter: Vec<Filter>) -> Self {
|
pub fn new(filter: Vec<Filter>) -> Self {
|
||||||
let subscription: Option<Subscription> = None;
|
let subscription: Option<Subscription> = None;
|
||||||
let notes = TimelineView::new(ViewFilter::Notes);
|
let notes = TimelineTab::new(ViewFilter::Notes);
|
||||||
let replies = TimelineView::new(ViewFilter::NotesAndReplies);
|
let replies = TimelineTab::new(ViewFilter::NotesAndReplies);
|
||||||
let views = vec![notes, replies];
|
let views = vec![notes, replies];
|
||||||
let selected_view = 0;
|
let selected_view = 0;
|
||||||
let routes = vec![Route::Timeline("Timeline".to_string())];
|
let routes = vec![Route::Timeline("Timeline".to_string())];
|
||||||
@@ -289,11 +284,11 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_view(&self) -> &TimelineView {
|
pub fn current_view(&self) -> &TimelineTab {
|
||||||
&self.views[self.selected_view as usize]
|
&self.views[self.selected_view as usize]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_view_mut(&mut self) -> &mut TimelineView {
|
pub fn current_view_mut(&mut self) -> &mut TimelineTab {
|
||||||
&mut self.views[self.selected_view as usize]
|
&mut self.views[self.selected_view as usize]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,202 +296,15 @@ impl Timeline {
|
|||||||
&self.views[view.index()].notes
|
&self.views[view.index()].notes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self, view: ViewFilter) -> &TimelineView {
|
pub fn view(&self, view: ViewFilter) -> &TimelineTab {
|
||||||
&self.views[view.index()]
|
&self.views[view.index()]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineView {
|
pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineTab {
|
||||||
&mut self.views[view.index()]
|
&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(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
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
*/
|
|
||||||
|
|
||||||
if timeline == 0 {
|
|
||||||
// show a postbox in the first timeline
|
|
||||||
|
|
||||||
if let Some(account) = app.account_manager.get_selected_account_index() {
|
|
||||||
if app
|
|
||||||
.account_manager
|
|
||||||
.get_selected_account()
|
|
||||||
.map_or(false, |a| a.secret_key.is_some())
|
|
||||||
{
|
|
||||||
if let Ok(txn) = Transaction::new(&app.ndb) {
|
|
||||||
let response =
|
|
||||||
ui::PostView::new(app, DraftSource::Compose, account).ui(&txn, ui);
|
|
||||||
|
|
||||||
if let Some(action) = response.action {
|
|
||||||
match action {
|
|
||||||
PostAction::Post(np) => {
|
|
||||||
let seckey = app
|
|
||||||
.account_manager
|
|
||||||
.get_account(account)
|
|
||||||
.unwrap()
|
|
||||||
.secret_key
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.to_secret_bytes();
|
|
||||||
|
|
||||||
let note = np.to_note(&seckey);
|
|
||||||
let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
|
|
||||||
info!("sending {}", raw_msg);
|
|
||||||
app.pool.send(&enostr::ClientMessage::raw(raw_msg));
|
|
||||||
app.drafts.clear(DraftSource::Compose);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.timelines[timeline].selected_view = tabs_ui(ui);
|
|
||||||
|
|
||||||
// need this for some reason??
|
|
||||||
ui.add_space(3.0);
|
|
||||||
|
|
||||||
let scroll_id = egui::Id::new(("tlscroll", app.timelines[timeline].selected_view, timeline));
|
|
||||||
egui::ScrollArea::vertical()
|
|
||||||
.id_source(scroll_id)
|
|
||||||
.animated(false)
|
|
||||||
.auto_shrink([false, 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
ui::padding(8.0, ui, |ui| {
|
|
||||||
let textmode = app.textmode;
|
|
||||||
let resp = ui::NoteView::new(app, ¬e)
|
|
||||||
.note_previews(!textmode)
|
|
||||||
.show(ui);
|
|
||||||
|
|
||||||
if let Some(action) = resp.action {
|
|
||||||
action.execute(app, timeline, note.id(), &txn);
|
|
||||||
} else if resp.response.clicked() {
|
|
||||||
debug!("clicked note");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui::hline(ui);
|
|
||||||
//ui.add(egui::Separator::default().spacing(0.0));
|
|
||||||
|
|
||||||
1
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum MergeKind {
|
pub enum MergeKind {
|
||||||
FrontInsert,
|
FrontInsert,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub mod profile;
|
|||||||
pub mod relay;
|
pub mod relay;
|
||||||
pub mod side_panel;
|
pub mod side_panel;
|
||||||
pub mod thread;
|
pub mod thread;
|
||||||
|
pub mod timeline;
|
||||||
pub mod username;
|
pub mod username;
|
||||||
|
|
||||||
pub use account_management::AccountManagementView;
|
pub use account_management::AccountManagementView;
|
||||||
@@ -24,6 +25,7 @@ pub use profile::{profile_preview_controller, ProfilePic, ProfilePreview};
|
|||||||
pub use relay::RelayView;
|
pub use relay::RelayView;
|
||||||
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
||||||
pub use thread::ThreadView;
|
pub use thread::ThreadView;
|
||||||
|
pub use timeline::TimelineView;
|
||||||
pub use username::Username;
|
pub use username::Username;
|
||||||
|
|
||||||
use egui::Margin;
|
use egui::Margin;
|
||||||
|
|||||||
228
src/ui/timeline.rs
Normal file
228
src/ui/timeline.rs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
use crate::{draft::DraftSource, ui, ui::note::PostAction, Damus};
|
||||||
|
use egui::containers::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{Direction, Layout};
|
||||||
|
use egui_tabs::TabColor;
|
||||||
|
use nostrdb::Transaction;
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
pub struct TimelineView<'a> {
|
||||||
|
app: &'a mut Damus,
|
||||||
|
reverse: bool,
|
||||||
|
timeline: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TimelineView<'a> {
|
||||||
|
pub fn new(app: &'a mut Damus, timeline: usize) -> TimelineView<'a> {
|
||||||
|
let reverse = false;
|
||||||
|
TimelineView {
|
||||||
|
app,
|
||||||
|
timeline,
|
||||||
|
reverse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
timeline_ui(ui, self.app, self.timeline, self.reverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reversed(mut self) -> Self {
|
||||||
|
self.reverse = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: 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;
|
||||||
|
*/
|
||||||
|
|
||||||
|
if timeline == 0 {
|
||||||
|
postbox_view(app, ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.timelines[timeline].selected_view = tabs_ui(ui);
|
||||||
|
|
||||||
|
// need this for some reason??
|
||||||
|
ui.add_space(3.0);
|
||||||
|
|
||||||
|
let scroll_id = egui::Id::new(("tlscroll", app.timelines[timeline].selected_view, timeline));
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.id_source(scroll_id)
|
||||||
|
.animated(false)
|
||||||
|
.auto_shrink([false, 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 ind = if reversed {
|
||||||
|
len - start_index - 1
|
||||||
|
} else {
|
||||||
|
start_index
|
||||||
|
};
|
||||||
|
|
||||||
|
let note_key = app.timelines[timeline].current_view().notes[ind].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;
|
||||||
|
};
|
||||||
|
|
||||||
|
ui::padding(8.0, ui, |ui| {
|
||||||
|
let textmode = app.textmode;
|
||||||
|
let resp = ui::NoteView::new(app, ¬e)
|
||||||
|
.note_previews(!textmode)
|
||||||
|
.show(ui);
|
||||||
|
|
||||||
|
if let Some(action) = resp.action {
|
||||||
|
action.execute(app, timeline, note.id(), &txn);
|
||||||
|
} else if resp.response.clicked() {
|
||||||
|
debug!("clicked note");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui::hline(ui);
|
||||||
|
//ui.add(egui::Separator::default().spacing(0.0));
|
||||||
|
|
||||||
|
1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn postbox_view(app: &mut Damus, ui: &mut egui::Ui) {
|
||||||
|
// show a postbox in the first timeline
|
||||||
|
|
||||||
|
if let Some(account) = app.account_manager.get_selected_account_index() {
|
||||||
|
if app
|
||||||
|
.account_manager
|
||||||
|
.get_selected_account()
|
||||||
|
.map_or(false, |a| a.secret_key.is_some())
|
||||||
|
{
|
||||||
|
if let Ok(txn) = Transaction::new(&app.ndb) {
|
||||||
|
let response = ui::PostView::new(app, DraftSource::Compose, account).ui(&txn, ui);
|
||||||
|
|
||||||
|
if let Some(action) = response.action {
|
||||||
|
match action {
|
||||||
|
PostAction::Post(np) => {
|
||||||
|
let seckey = app
|
||||||
|
.account_manager
|
||||||
|
.get_account(account)
|
||||||
|
.unwrap()
|
||||||
|
.secret_key
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.to_secret_bytes();
|
||||||
|
|
||||||
|
let note = np.to_note(&seckey);
|
||||||
|
let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
|
||||||
|
info!("sending {}", raw_msg);
|
||||||
|
app.pool.send(&enostr::ClientMessage::raw(raw_msg));
|
||||||
|
app.drafts.clear(DraftSource::Compose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user