Merge Persist Columns #390

Fixed a few merge conflicts

kernelkind (2):
      initial column storage
      tmp remove DeckAuthor columns
This commit is contained in:
William Casarin
2024-11-13 13:13:41 -08:00
12 changed files with 208 additions and 31 deletions

View File

@@ -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,

View File

@@ -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());
}
});
}

View File

@@ -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>),

View File

@@ -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
}
}

View File

@@ -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) {

View File

@@ -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),

48
src/storage/columns.rs Normal file
View File

@@ -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
}
}
}

View File

@@ -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};

View File

@@ -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),

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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",