From 56dd88b3a21c00fb526296d277ac6bfb807ae11c Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 29 Oct 2024 17:23:40 -0400 Subject: [PATCH 1/2] initial column storage Signed-off-by: kernelkind --- src/account_manager.rs | 3 +- src/app.rs | 41 +++++++++++++++----- src/args.rs | 7 +--- src/column.rs | 88 +++++++++++++++++++++++++++++++++++++++++- src/nav.rs | 7 +++- src/route.rs | 3 +- src/storage/columns.rs | 60 ++++++++++++++++++++++++++++ src/storage/mod.rs | 2 + src/timeline/kind.rs | 7 ++-- src/timeline/mod.rs | 22 ++++++++++- src/timeline/route.rs | 2 +- 11 files changed, 217 insertions(+), 25 deletions(-) create mode 100644 src/storage/columns.rs diff --git a/src/account_manager.rs b/src/account_manager.rs index 7551d1a6..42d2e517 100644 --- a/src/account_manager.rs +++ b/src/account_manager.rs @@ -2,6 +2,7 @@ use std::cmp::Ordering; use enostr::{FilledKeypair, FullKeypair, Keypair}; use nostrdb::Ndb; +use serde::{Deserialize, Serialize}; use crate::{ column::Columns, @@ -32,7 +33,7 @@ pub enum AccountsRouteResponse { AddAccount(AccountLoginResponse), } -#[derive(Debug, Eq, PartialEq, Clone, Copy)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] pub enum AccountsRoute { Accounts, AddAccount, diff --git a/src/app.rs b/src/app.rs index 0f475170..4c51b41b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -15,7 +15,7 @@ use crate::{ notecache::{CachedNote, NoteCache}, notes_holder::NotesHolderStorage, profile::Profile, - storage::{Directory, FileKeyStorage, KeyStorageType}, + storage::{self, Directory, FileKeyStorage, KeyStorageType}, subscriptions::{SubKind, Subscriptions}, support::Support, thread::Thread, @@ -727,12 +727,28 @@ impl Damus { .map(|a| a.pubkey.bytes()); let ndb = Ndb::new(&dbpath, &config).expect("ndb"); - let mut columns: Columns = Columns::new(); - for col in parsed_args.columns { - if let Some(timeline) = col.into_timeline(&ndb, account) { - columns.add_new_timeline_column(timeline); + let mut columns = if parsed_args.columns.is_empty() { + if let Some(serializable_columns) = storage::load_columns() { + info!("Using columns from disk"); + serializable_columns.into_columns(&ndb, account) + } else { + info!("Could not load columns from disk"); + Columns::new() } - } + } else { + info!( + "Using columns from command line arguments: {:?}", + parsed_args.columns + ); + let mut columns: Columns = Columns::new(); + for col in parsed_args.columns { + if let Some(timeline) = col.into_timeline(&ndb, account) { + columns.add_new_timeline_column(timeline); + } + } + + columns + }; let debug = parsed_args.debug; @@ -971,8 +987,8 @@ fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { //let routes = app.timelines[0].routes.clone(); main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { - if !app.columns.columns().is_empty() { - nav::render_nav(0, app, ui); + if !app.columns.columns().is_empty() && nav::render_nav(0, app, ui) { + storage::save_columns(app.columns.as_serializable_columns()); } }); } @@ -1049,10 +1065,13 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { ); }); + let mut columns_changed = false; for col_index in 0..app.columns.num_columns() { strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); - nav::render_nav(col_index, app, ui); + if nav::render_nav(col_index, app, ui) { + columns_changed = true; + } // vertical line ui.painter().vline( @@ -1064,6 +1083,10 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { //strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind)); } + + if columns_changed { + storage::save_columns(app.columns.as_serializable_columns()); + } }); } diff --git a/src/args.rs b/src/args.rs index 9ac7d923..8170b149 100644 --- a/src/args.rs +++ b/src/args.rs @@ -219,18 +219,13 @@ impl Args { i += 1; } - if res.columns.is_empty() { - let ck = TimelineKind::contact_list(PubkeySource::DeckAuthor); - info!("No columns set, setting up defaults: {:?}", ck); - res.columns.push(ArgColumn::Timeline(ck)); - } - res } } /// A way to define columns from the commandline. Can be column kinds or /// generic queries +#[derive(Debug)] pub enum ArgColumn { Timeline(TimelineKind), Generic(Vec), diff --git a/src/column.rs b/src/column.rs index 0b986323..3b513d05 100644 --- a/src/column.rs +++ b/src/column.rs @@ -1,10 +1,13 @@ use crate::route::{Route, Router}; -use crate::timeline::{Timeline, TimelineId}; +use crate::timeline::{SerializableTimeline, Timeline, TimelineId, TimelineRoute}; use indexmap::IndexMap; +use nostrdb::Ndb; +use serde::{Deserialize, Deserializer, Serialize}; use std::iter::Iterator; use std::sync::atomic::{AtomicU32, Ordering}; -use tracing::warn; +use tracing::{error, warn}; +#[derive(Clone)] pub struct Column { router: Router, } @@ -24,6 +27,28 @@ impl Column { } } +impl serde::Serialize for Column { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.router.routes().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Column { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let routes = Vec::::deserialize(deserializer)?; + + Ok(Column { + router: Router::new(routes), + }) + } +} + #[derive(Default)] pub struct Columns { /// Columns are simply routers into settings, timelines, etc @@ -68,6 +93,10 @@ impl Columns { UIDS.fetch_add(1, Ordering::Relaxed) } + pub fn add_column_at(&mut self, column: Column, index: u32) { + self.columns.insert(index, column); + } + pub fn add_column(&mut self, column: Column) { self.columns.insert(Self::get_new_id(), column); } @@ -194,4 +223,59 @@ impl Columns { } } } + + pub fn as_serializable_columns(&self) -> SerializableColumns { + SerializableColumns { + columns: self.columns.values().cloned().collect(), + timelines: self + .timelines + .values() + .map(|t| t.as_serializable_timeline()) + .collect(), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct SerializableColumns { + pub columns: Vec, + pub timelines: Vec, +} + +impl SerializableColumns { + pub fn into_columns(self, ndb: &Ndb, deck_pubkey: Option<&[u8; 32]>) -> Columns { + let mut columns = Columns::default(); + + for column in self.columns { + let id = Columns::get_new_id(); + let mut routes = Vec::new(); + for route in column.router.routes() { + match route { + Route::Timeline(TimelineRoute::Timeline(timeline_id)) => { + if let Some(serializable_tl) = + self.timelines.iter().find(|tl| tl.id == *timeline_id) + { + let tl = serializable_tl.clone().into_timeline(ndb, deck_pubkey); + if let Some(tl) = tl { + routes.push(Route::Timeline(TimelineRoute::Timeline(tl.id))); + columns.timelines.insert(id, tl); + } else { + error!("Problem deserializing timeline {:?}", serializable_tl); + } + } + } + Route::Timeline(TimelineRoute::Thread(_thread)) => { + // TODO: open thread before pushing route + } + Route::Profile(_profile) => { + // TODO: open profile before pushing route + } + _ => routes.push(*route), + } + } + columns.add_column_at(Column::new(routes), id); + } + + columns + } } diff --git a/src/nav.rs b/src/nav.rs index efe1283a..5f8f2141 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -27,7 +27,8 @@ use egui_nav::{Nav, NavAction, TitleBarResponse}; use nostrdb::{Ndb, Transaction}; use tracing::{error, info}; -pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { +pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> bool { + let mut col_changed = false; let col_id = app.columns.get_column_id_at_index(col); // TODO(jb55): clean up this router_mut mess by using Router in egui-nav directly let routes = app @@ -201,12 +202,14 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { pubkey.bytes(), ); } + col_changed = true; } else if let Some(NavAction::Navigated) = nav_response.action { let cur_router = app.columns_mut().column_mut(col).router_mut(); cur_router.navigating = false; if cur_router.is_replacing() { cur_router.remove_previous_route(); } + col_changed = true; } if let Some(title_response) = nav_response.title_response { @@ -220,6 +223,8 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { } } } + + col_changed } fn unsubscribe_timeline(ndb: &Ndb, timeline: &Timeline) { diff --git a/src/route.rs b/src/route.rs index 27923fb3..0f7a7a12 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,5 +1,6 @@ use enostr::{NoteId, Pubkey}; use nostrdb::Ndb; +use serde::{Deserialize, Serialize}; use std::fmt::{self}; use crate::{ @@ -10,7 +11,7 @@ use crate::{ }; /// App routing. These describe different places you can go inside Notedeck. -#[derive(Clone, Copy, Eq, PartialEq, Debug)] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)] pub enum Route { Timeline(TimelineRoute), Accounts(AccountsRoute), diff --git a/src/storage/columns.rs b/src/storage/columns.rs new file mode 100644 index 00000000..1fe542f8 --- /dev/null +++ b/src/storage/columns.rs @@ -0,0 +1,60 @@ +use tracing::{error, info}; + +use crate::column::SerializableColumns; + +use super::{write_file, DataPaths, Directory}; + +static COLUMNS_FILE: &str = "columns.json"; + +pub fn save_columns(columns: SerializableColumns) { + let serialized_columns = match serde_json::to_string(&columns) { + Ok(s) => s, + Err(e) => { + error!("Could not serialize columns: {}", e); + return; + } + }; + + let data_path = match DataPaths::Setting.get_path() { + Ok(s) => s, + Err(e) => { + error!("Could not get data path: {}", e); + return; + } + }; + + if let Err(e) = write_file(&data_path, COLUMNS_FILE.to_string(), &serialized_columns) { + error!("Could not write columns to file {}: {}", COLUMNS_FILE, e); + } else { + info!("Successfully wrote columns to {}", COLUMNS_FILE); + } +} + +pub fn load_columns() -> Option { + let data_path = match DataPaths::Setting.get_path() { + Ok(s) => s, + Err(e) => { + error!("Could not get data path: {}", e); + return None; + } + }; + + let columns_string = match Directory::new(data_path).get_file(COLUMNS_FILE.to_owned()) { + Ok(s) => s, + Err(e) => { + error!("Could not read columns from file {}: {}", COLUMNS_FILE, e); + return None; + } + }; + + match serde_json::from_str::(&columns_string) { + Ok(s) => { + info!("Successfully loaded columns from {}", COLUMNS_FILE); + Some(s) + } + Err(e) => { + error!("Could not deserialize columns: {}", e); + None + } + } +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 7eb4ce7e..1a5a9e22 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,7 +1,9 @@ +mod columns; #[cfg(any(target_os = "linux", target_os = "macos"))] mod file_key_storage; mod file_storage; +pub use columns::{load_columns, save_columns}; pub use file_key_storage::FileKeyStorage; pub use file_storage::write_file; pub use file_storage::DataPaths; diff --git a/src/timeline/kind.rs b/src/timeline/kind.rs index c48a9279..3a281dd3 100644 --- a/src/timeline/kind.rs +++ b/src/timeline/kind.rs @@ -5,16 +5,17 @@ use crate::timeline::Timeline; use crate::ui::profile::preview::get_profile_displayname_string; use enostr::{Filter, Pubkey}; use nostrdb::{Ndb, Transaction}; +use serde::{Deserialize, Serialize}; use std::fmt::Display; use tracing::{error, warn}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum PubkeySource { Explicit(Pubkey), DeckAuthor, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum ListKind { Contact(PubkeySource), } @@ -27,7 +28,7 @@ pub enum ListKind { /// - filter /// - ... etc /// -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum TimelineKind { List(ListKind), diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs index 72593375..d92e0f97 100644 --- a/src/timeline/mod.rs +++ b/src/timeline/mod.rs @@ -9,6 +9,7 @@ use std::sync::atomic::{AtomicU32, Ordering}; use egui_virtual_list::VirtualList; use nostrdb::{Ndb, Note, Subscription, Transaction}; +use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::hash::Hash; use std::rc::Rc; @@ -21,7 +22,7 @@ pub mod route; pub use kind::{PubkeySource, TimelineKind}; pub use route::TimelineRoute; -#[derive(Debug, Hash, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Hash, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TimelineId(u32); impl TimelineId { @@ -177,6 +178,18 @@ pub struct Timeline { pub subscription: Option, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SerializableTimeline { + pub id: TimelineId, + pub kind: TimelineKind, +} + +impl SerializableTimeline { + pub fn into_timeline(self, ndb: &Ndb, deck_user_pubkey: Option<&[u8; 32]>) -> Option { + self.kind.into_timeline(ndb, deck_user_pubkey) + } +} + impl Timeline { /// Create a timeline from a contact list pub fn contact_list(contact_list: &Note, pk_src: PubkeySource) -> Result { @@ -312,6 +325,13 @@ impl Timeline { Ok(()) } + + pub fn as_serializable_timeline(&self) -> SerializableTimeline { + SerializableTimeline { + id: self.id, + kind: self.kind.clone(), + } + } } pub enum MergeKind { diff --git a/src/timeline/route.rs b/src/timeline/route.rs index 073479c6..059989f0 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -21,7 +21,7 @@ use crate::{ use enostr::{NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; -#[derive(Debug, Eq, PartialEq, Clone, Copy)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)] pub enum TimelineRoute { Timeline(TimelineId), Thread(NoteId), From ee5dd5426fbb9b23329ad122beb76ee1baed9708 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 30 Oct 2024 13:45:22 -0400 Subject: [PATCH 2/2] tmp remove DeckAuthor columns we don't yet have logic for handling switching 'deck authors' and this is causing two problems: 1. the column title isn't renamed when the selected account is changed 2. when saving a deck author column to disk and the account is switched beforehand, it switches to the current deck author's column Signed-off-by: kernelkind --- src/ui/add_column.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ui/add_column.rs b/src/ui/add_column.rs index 298addcc..a25d4b83 100644 --- a/src/ui/add_column.rs +++ b/src/ui/add_column.rs @@ -178,11 +178,7 @@ impl<'a> AddColumnView<'a> { }); if let Some(acc) = self.cur_account { - let source = if acc.secret_key.is_some() { - PubkeySource::DeckAuthor - } else { - PubkeySource::Explicit(acc.pubkey) - }; + let source = PubkeySource::Explicit(acc.pubkey); vec.push(ColumnOptionData { title: "Home timeline",