notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 91016facc720a3eb2b65f907fbcc7eb0f3ff4163
parent 94b97d247d204073727ca107ff5a433a520fed4c
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 13 Nov 2024 13:13:41 -0800

Merge Persist Columns #390

Fixed a few merge conflicts

kernelkind (2):
      initial column storage
      tmp remove DeckAuthor columns

Diffstat:
Msrc/account_manager.rs | 3++-
Msrc/app.rs | 41++++++++++++++++++++++++++++++++---------
Msrc/args.rs | 7+------
Msrc/column.rs | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/nav.rs | 7++++++-
Msrc/route.rs | 3++-
Asrc/storage/columns.rs | 48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/storage/mod.rs | 2++
Msrc/timeline/kind.rs | 7++++---
Msrc/timeline/mod.rs | 22+++++++++++++++++++++-
Msrc/timeline/route.rs | 2+-
Msrc/ui/add_column.rs | 9+++------
12 files changed, 208 insertions(+), 31 deletions(-)

diff --git 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 @@ -15,7 +15,7 @@ use crate::{ notecache::{CachedNote, NoteCache}, notes_holder::NotesHolderStorage, profile::Profile, - storage::{DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageType}, + storage::{self, DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageType}, subscriptions::{SubKind, Subscriptions}, support::Support, thread::Thread, @@ -721,12 +721,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(&path) { + 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; @@ -982,8 +998,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.path, app.columns.as_serializable_columns()); } }); } @@ -1060,10 +1076,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( @@ -1075,6 +1094,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.path, app.columns.as_serializable_columns()); + } }); } diff --git 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<Filter>), diff --git 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<Route>, } @@ -24,6 +27,28 @@ impl Column { } } +impl serde::Serialize for Column { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + self.router.routes().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Column { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let routes = Vec::<Route>::deserialize(deserializer)?; + + Ok(Column { + router: Router::new(routes), + }) + } +} + #[derive(Default)] pub struct Columns { /// Columns are simply routers into settings, timelines, etc @@ -70,6 +95,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); } @@ -196,4 +225,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<Column>, + pub timelines: Vec<SerializableTimeline>, +} + +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 @@ -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<R> in egui-nav directly let routes = app @@ -191,12 +192,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_routes(); } + col_changed = true; } if let Some(title_response) = nav_response.title_response { @@ -210,6 +213,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 @@ -1,5 +1,6 @@ use enostr::{NoteId, Pubkey}; use nostrdb::Ndb; +use serde::{Deserialize, Serialize}; use std::fmt::{self}; use crate::{ @@ -13,7 +14,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 @@ -0,0 +1,48 @@ +use tracing::{error, info}; + +use crate::column::SerializableColumns; + +use super::{write_file, DataPath, DataPathType, Directory}; + +static COLUMNS_FILE: &str = "columns.json"; + +pub fn save_columns(path: &DataPath, 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 = path.path(DataPathType::Setting); + + 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(path: &DataPath) -> Option<SerializableColumns> { + let data_path = path.path(DataPathType::Setting); + + 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::<SerializableColumns>(&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 @@ -1,6 +1,8 @@ +mod columns; mod file_key_storage; mod file_storage; +pub use columns::{load_columns, save_columns}; pub use file_key_storage::FileKeyStorage; pub use file_storage::{delete_file, write_file, DataPath, DataPathType, Directory}; diff --git 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, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum PubkeySource { Explicit(Pubkey), DeckAuthor, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ListKind { Contact(PubkeySource), } @@ -27,7 +28,7 @@ pub enum ListKind { /// - filter /// - ... etc /// -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum TimelineKind { List(ListKind), diff --git 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<Subscription>, } +#[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<Timeline> { + 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<Self> { @@ -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 @@ -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), diff --git a/src/ui/add_column.rs b/src/ui/add_column.rs @@ -1,4 +1,5 @@ use core::f32; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use egui::{ @@ -39,7 +40,7 @@ enum AddColumnOption { Home(PubkeySource), } -#[derive(Clone, Copy, Eq, PartialEq, Debug)] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)] pub enum AddColumnRoute { Base, UndecidedNotification, @@ -276,11 +277,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",