split notedeck into crates

This splits notedeck into crates, separating the browser chrome and
individual apps:

* notedeck: binary file, browser chrome
* notedeck_columns: our columns app
* enostr: same as before

We still need to do more work to cleanly separate the chrome apis
from the app apis. Soon I will create notedeck-notebook to see what
makes sense to be shared between the apps.

Some obvious ones that come to mind:

1. ImageCache

We will likely want to move this to the notedeck crate, as most apps
will want some kind of image cache. In web browsers, web pages do not
need to worry about this, so we will likely have to do something similar

2. Ndb

Since NdbRef is threadsafe and Ndb is an Arc<NdbRef>, it can be safely
copied to each app. This will simplify things. In the future we might
want to create an abstraction over this? Maybe each app shouldn't have
access to the same database... we assume the data in DBs are all public
anyways, but if we have unwrapped giftwraps that could be a problem.

3. RelayPool / Subscription Manager

The browser should probably maintain these. Then apps can use ken's
high level subscription manager api and not have to worry about
connection pool details

4. Accounts

Accounts and key management should be handled by the chrome. Apps should
only have a simple signer interface.

That's all for now, just something to think about!

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2024-12-11 02:53:05 -08:00
parent 10cbdf15f0
commit 74c5f0c748
156 changed files with 194 additions and 252 deletions

View File

@@ -0,0 +1,389 @@
use crate::{
accounts::{render_accounts_route, AccountsAction},
actionbar::NoteAction,
app::{get_active_columns, get_active_columns_mut, get_decks_mut},
column::ColumnsAction,
deck_state::DeckState,
decks::{Deck, DecksAction},
notes_holder::NotesHolder,
profile::Profile,
relay_pool_manager::RelayPoolManager,
route::Route,
thread::Thread,
timeline::{
route::{render_timeline_route, TimelineRoute},
Timeline,
},
ui::{
self,
add_column::render_add_column_routes,
column::NavTitle,
configure_deck::ConfigureDeckView,
edit_deck::{EditDeckResponse, EditDeckView},
note::{PostAction, PostType},
support::SupportView,
RelayView, View,
},
Damus,
};
use egui_nav::{Nav, NavAction, NavResponse, NavUiType};
use nostrdb::{Ndb, Transaction};
use tracing::{error, info};
#[allow(clippy::enum_variant_names)]
pub enum RenderNavAction {
Back,
RemoveColumn,
PostAction(PostAction),
NoteAction(NoteAction),
SwitchingAction(SwitchingAction),
}
pub enum SwitchingAction {
Accounts(AccountsAction),
Columns(ColumnsAction),
Decks(crate::decks::DecksAction),
}
impl SwitchingAction {
/// process the action, and return whether switching occured
pub fn process(&self, app: &mut Damus) -> bool {
match &self {
SwitchingAction::Accounts(account_action) => match *account_action {
AccountsAction::Switch(index) => app.accounts.select_account(index),
AccountsAction::Remove(index) => app.accounts.remove_account(index),
},
SwitchingAction::Columns(columns_action) => match *columns_action {
ColumnsAction::Remove(index) => {
get_active_columns_mut(&app.accounts, &mut app.decks_cache).delete_column(index)
}
},
SwitchingAction::Decks(decks_action) => match *decks_action {
DecksAction::Switch(index) => {
get_decks_mut(&app.accounts, &mut app.decks_cache).set_active(index)
}
DecksAction::Removing(index) => {
get_decks_mut(&app.accounts, &mut app.decks_cache).remove_deck(index)
}
},
}
true
}
}
impl From<PostAction> for RenderNavAction {
fn from(post_action: PostAction) -> Self {
Self::PostAction(post_action)
}
}
impl From<NoteAction> for RenderNavAction {
fn from(note_action: NoteAction) -> RenderNavAction {
Self::NoteAction(note_action)
}
}
pub type NotedeckNavResponse = NavResponse<Option<RenderNavAction>>;
pub struct RenderNavResponse {
column: usize,
response: NotedeckNavResponse,
}
impl RenderNavResponse {
#[allow(private_interfaces)]
pub fn new(column: usize, response: NotedeckNavResponse) -> Self {
RenderNavResponse { column, response }
}
#[must_use = "Make sure to save columns if result is true"]
pub fn process_render_nav_response(&self, app: &mut Damus) -> bool {
let mut switching_occured: bool = false;
let col = self.column;
if let Some(action) = self
.response
.response
.as_ref()
.or(self.response.title_response.as_ref())
{
// start returning when we're finished posting
match action {
RenderNavAction::Back => {
app.columns_mut().column_mut(col).router_mut().go_back();
}
RenderNavAction::RemoveColumn => {
let tl = app.columns().find_timeline_for_column_index(col);
if let Some(timeline) = tl {
unsubscribe_timeline(app.ndb(), timeline);
}
app.columns_mut().delete_column(col);
switching_occured = true;
}
RenderNavAction::PostAction(post_action) => {
let txn = Transaction::new(&app.ndb).expect("txn");
let _ = post_action.execute(&app.ndb, &txn, &mut app.pool, &mut app.drafts);
get_active_columns_mut(&app.accounts, &mut app.decks_cache)
.column_mut(col)
.router_mut()
.go_back();
}
RenderNavAction::NoteAction(note_action) => {
let txn = Transaction::new(&app.ndb).expect("txn");
note_action.execute_and_process_result(
&app.ndb,
get_active_columns_mut(&app.accounts, &mut app.decks_cache),
col,
&mut app.threads,
&mut app.profiles,
&mut app.note_cache,
&mut app.pool,
&txn,
&app.accounts.mutefun(),
);
}
RenderNavAction::SwitchingAction(switching_action) => {
switching_occured = switching_action.process(app);
}
}
}
if let Some(action) = self.response.action {
match action {
NavAction::Returned => {
let r = app.columns_mut().column_mut(col).router_mut().pop();
let txn = Transaction::new(&app.ndb).expect("txn");
if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r {
let root_id = {
crate::note::root_note_id_from_selected_id(
&app.ndb,
&mut app.note_cache,
&txn,
id.bytes(),
)
};
Thread::unsubscribe_locally(
&txn,
&app.ndb,
&mut app.note_cache,
&mut app.threads,
&mut app.pool,
root_id,
&app.accounts.mutefun(),
);
}
if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r {
Profile::unsubscribe_locally(
&txn,
&app.ndb,
&mut app.note_cache,
&mut app.profiles,
&mut app.pool,
pubkey.bytes(),
&app.accounts.mutefun(),
);
}
switching_occured = true;
}
NavAction::Navigated => {
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();
}
switching_occured = true;
}
NavAction::Dragging => {}
NavAction::Returning => {}
NavAction::Resetting => {}
NavAction::Navigating => {}
}
}
switching_occured
}
}
fn render_nav_body(
ui: &mut egui::Ui,
app: &mut Damus,
top: &Route,
col: usize,
) -> Option<RenderNavAction> {
match top {
Route::Timeline(tlr) => render_timeline_route(
&app.ndb,
get_active_columns_mut(&app.accounts, &mut app.decks_cache),
&mut app.drafts,
&mut app.img_cache,
&mut app.unknown_ids,
&mut app.note_cache,
&mut app.threads,
&mut app.profiles,
&mut app.accounts,
*tlr,
col,
app.textmode,
ui,
),
Route::Accounts(amr) => {
let mut action = render_accounts_route(
ui,
&app.ndb,
col,
&mut app.img_cache,
&mut app.accounts,
&mut app.decks_cache,
&mut app.view_state.login,
*amr,
);
let txn = Transaction::new(&app.ndb).expect("txn");
action.process_action(&mut app.unknown_ids, &app.ndb, &txn);
action
.accounts_action
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
}
Route::Relays => {
let manager = RelayPoolManager::new(app.pool_mut());
RelayView::new(manager).ui(ui);
None
}
Route::ComposeNote => {
let kp = app.accounts.get_selected_account()?.to_full()?;
let draft = app.drafts.compose_mut();
let txn = Transaction::new(&app.ndb).expect("txn");
let post_response = ui::PostView::new(
&app.ndb,
draft,
PostType::New,
&mut app.img_cache,
&mut app.note_cache,
kp,
)
.ui(&txn, ui);
post_response.action.map(Into::into)
}
Route::AddColumn(route) => {
render_add_column_routes(ui, app, col, route);
None
}
Route::Support => {
SupportView::new(&mut app.support).show(ui);
None
}
Route::NewDeck => {
let id = ui.id().with("new-deck");
let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
let mut resp = None;
if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) {
if let Some(cur_acc) = app.accounts.get_selected_account() {
app.decks_cache.add_deck(
cur_acc.pubkey,
Deck::new(config_resp.icon, config_resp.name),
);
// set new deck as active
let cur_index = get_decks_mut(&app.accounts, &mut app.decks_cache)
.decks()
.len()
- 1;
resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
DecksAction::Switch(cur_index),
)));
}
new_deck_state.clear();
get_active_columns_mut(&app.accounts, &mut app.decks_cache)
.get_first_router()
.go_back();
}
resp
}
Route::EditDeck(index) => {
let mut action = None;
let cur_deck = get_decks_mut(&app.accounts, &mut app.decks_cache)
.decks_mut()
.get_mut(*index)
.expect("index wasn't valid");
let id = ui.id().with((
"edit-deck",
app.accounts.get_selected_account().map(|k| k.pubkey),
index,
));
let deck_state = app
.view_state
.id_to_deck_state
.entry(id)
.or_insert_with(|| DeckState::from_deck(cur_deck));
if let Some(resp) = EditDeckView::new(deck_state).ui(ui) {
match resp {
EditDeckResponse::Edit(configure_deck_response) => {
cur_deck.edit(configure_deck_response);
}
EditDeckResponse::Delete => {
action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
DecksAction::Removing(*index),
)));
}
}
get_active_columns_mut(&app.accounts, &mut app.decks_cache)
.get_first_router()
.go_back();
}
action
}
}
}
#[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> RenderNavResponse {
let col_id = get_active_columns(&app.accounts, &app.decks_cache).get_column_id_at_index(col);
// TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly
let nav_response = Nav::new(&app.columns().column(col).router().routes().clone())
.navigating(app.columns_mut().column_mut(col).router_mut().navigating)
.returning(app.columns_mut().column_mut(col).router_mut().returning)
.id_source(egui::Id::new(col_id))
.show_mut(ui, |ui, render_type, nav| match render_type {
NavUiType::Title => NavTitle::new(
&app.ndb,
&mut app.img_cache,
get_active_columns_mut(&app.accounts, &mut app.decks_cache),
app.accounts.get_selected_account().map(|a| &a.pubkey),
nav.routes(),
)
.show(ui),
NavUiType::Body => render_nav_body(ui, app, nav.routes().last().expect("top"), col),
});
RenderNavResponse::new(col, nav_response)
}
fn unsubscribe_timeline(ndb: &Ndb, timeline: &Timeline) {
if let Some(sub_id) = timeline.subscription {
if let Err(e) = ndb.unsubscribe(sub_id) {
error!("unsubscribe error: {}", e);
} else {
info!(
"successfully unsubscribed from timeline {} with sub id {}",
timeline.id,
sub_id.id()
);
}
}
}