Flexible routing

Another massive refactor to change the way routing works. Now any
column can route anywhere.

Also things are generally just much better and more modular via the
new struct split borrowing technique.

I didn't even try to split this into smaller commits for my sanity.

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2024-09-11 19:43:41 -07:00
parent b4a8cddc48
commit 36c0971fd9
27 changed files with 973 additions and 963 deletions

1
.envrc
View File

@@ -12,3 +12,4 @@ export JB55=32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
export JACK=npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m export JACK=npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m
export VROD=npub1h50pnxqw9jg7dhr906fvy4mze2yzawf895jhnc3p7qmljdugm6gsrurqev export VROD=npub1h50pnxqw9jg7dhr906fvy4mze2yzawf895jhnc3p7qmljdugm6gsrurqev
export JEFFG=npub1zuuajd7u3sx8xu92yav9jwxpr839cs0kc3q6t56vd5u9q033xmhsk6c2uc

View File

@@ -1,16 +1,23 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use enostr::{FilledKeypair, FullKeypair, Keypair}; use enostr::{FilledKeypair, FullKeypair, Keypair};
use nostrdb::Ndb;
pub use crate::user_account::UserAccount;
use crate::{ use crate::{
column::Columns,
imgcache::ImageCache,
key_storage::{KeyStorage, KeyStorageResponse, KeyStorageType}, key_storage::{KeyStorage, KeyStorageResponse, KeyStorageType},
login_manager::LoginState,
route::{Route, Router},
ui::{ ui::{
account_login_view::AccountLoginResponse, account_management::AccountManagementViewResponse, account_login_view::{AccountLoginResponse, AccountLoginView},
account_management::{AccountsView, AccountsViewResponse},
}, },
}; };
use tracing::info; use tracing::info;
pub use crate::user_account::UserAccount;
/// The interface for managing the user's accounts. /// The interface for managing the user's accounts.
/// Represents all user-facing operations related to account management. /// Represents all user-facing operations related to account management.
pub struct AccountManager { pub struct AccountManager {
@@ -19,6 +26,75 @@ pub struct AccountManager {
key_store: KeyStorageType, key_store: KeyStorageType,
} }
// TODO(jb55): move to accounts/route.rs
pub enum AccountsRouteResponse {
Accounts(AccountsViewResponse),
AddAccount(AccountLoginResponse),
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum AccountsRoute {
Accounts,
AddAccount,
}
/// Render account management views from a route
#[allow(clippy::too_many_arguments)]
pub fn render_accounts_route(
ui: &mut egui::Ui,
ndb: &Ndb,
col: usize,
columns: &mut Columns,
img_cache: &mut ImageCache,
accounts: &mut AccountManager,
login_state: &mut LoginState,
route: AccountsRoute,
) {
let router = columns.column_mut(col).router_mut();
let resp = match route {
AccountsRoute::Accounts => AccountsView::new(ndb, accounts, img_cache)
.ui(ui)
.inner
.map(AccountsRouteResponse::Accounts),
AccountsRoute::AddAccount => AccountLoginView::new(login_state)
.ui(ui)
.inner
.map(AccountsRouteResponse::AddAccount),
};
if let Some(resp) = resp {
match resp {
AccountsRouteResponse::Accounts(response) => {
process_accounts_view_response(accounts, response, router);
}
AccountsRouteResponse::AddAccount(response) => {
process_login_view_response(accounts, response);
*login_state = Default::default();
router.go_back();
}
}
}
}
pub fn process_accounts_view_response(
manager: &mut AccountManager,
response: AccountsViewResponse,
router: &mut Router<Route>,
) {
match response {
AccountsViewResponse::RemoveAccount(index) => {
manager.remove_account(index);
}
AccountsViewResponse::SelectAccount(index) => {
manager.select_account(index);
}
AccountsViewResponse::RouteToLogin => {
router.route_to(Route::add_account());
}
}
}
impl AccountManager { impl AccountManager {
pub fn new(currently_selected_account: Option<usize>, key_store: KeyStorageType) -> Self { pub fn new(currently_selected_account: Option<usize>, key_store: KeyStorageType) -> Self {
let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() { let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() {
@@ -122,21 +198,6 @@ impl AccountManager {
} }
} }
pub fn process_management_view_response_stateless(
manager: &mut AccountManager,
response: AccountManagementViewResponse,
) {
match response {
AccountManagementViewResponse::RemoveAccount(index) => {
manager.remove_account(index);
}
AccountManagementViewResponse::SelectAccount(index) => {
manager.select_account(index);
}
AccountManagementViewResponse::RouteToLogin => {}
}
}
pub fn process_login_view_response(manager: &mut AccountManager, response: AccountLoginResponse) { pub fn process_login_view_response(manager: &mut AccountManager, response: AccountLoginResponse) {
match response { match response {
AccountLoginResponse::CreateNew => { AccountLoginResponse::CreateNew => {

View File

@@ -1,8 +1,7 @@
use crate::{ use crate::{
column::Column,
note::NoteRef, note::NoteRef,
notecache::NoteCache, notecache::NoteCache,
route::Route, route::{Route, Router},
thread::{Thread, ThreadResult, Threads}, thread::{Thread, ThreadResult, Threads},
}; };
use enostr::{NoteId, RelayPool}; use enostr::{NoteId, RelayPool};
@@ -12,8 +11,8 @@ use uuid::Uuid;
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum BarAction { pub enum BarAction {
Reply, Reply(NoteId),
OpenThread, OpenThread(NoteId),
} }
pub struct NewThreadNotes { pub struct NewThreadNotes {
@@ -33,17 +32,15 @@ pub enum BarResult {
fn open_thread( fn open_thread(
ndb: &Ndb, ndb: &Ndb,
txn: &Transaction, txn: &Transaction,
column: &mut Column, router: &mut Router<Route>,
note_cache: &mut NoteCache, note_cache: &mut NoteCache,
pool: &mut RelayPool, pool: &mut RelayPool,
threads: &mut Threads, threads: &mut Threads,
selected_note: &[u8; 32], selected_note: &[u8; 32],
) -> Option<BarResult> { ) -> Option<BarResult> {
{ {
column router.route_to(Route::thread(NoteId::new(selected_note.to_owned())));
.routes_mut() router.navigating = true;
.push(Route::Thread(NoteId::new(selected_note.to_owned())));
column.navigating = true;
} }
let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, txn, selected_note); let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, txn, selected_note);
@@ -52,7 +49,7 @@ fn open_thread(
let (thread, result) = match thread_res { let (thread, result) = match thread_res {
ThreadResult::Stale(thread) => { ThreadResult::Stale(thread) => {
// The thread is stale, let's update it // The thread is stale, let's update it
let notes = Thread::new_notes(&thread.view.notes, root_id, txn, ndb); let notes = Thread::new_notes(&thread.view().notes, root_id, txn, ndb);
let bar_result = if notes.is_empty() { let bar_result = if notes.is_empty() {
None None
} else { } else {
@@ -120,33 +117,57 @@ impl BarAction {
pub fn execute( pub fn execute(
self, self,
ndb: &Ndb, ndb: &Ndb,
column: &mut Column, router: &mut Router<Route>,
threads: &mut Threads, threads: &mut Threads,
note_cache: &mut NoteCache, note_cache: &mut NoteCache,
pool: &mut RelayPool, pool: &mut RelayPool,
replying_to: &[u8; 32],
txn: &Transaction, txn: &Transaction,
) -> Option<BarResult> { ) -> Option<BarResult> {
match self { match self {
BarAction::Reply => { BarAction::Reply(note_id) => {
column router.route_to(Route::reply(note_id));
.routes_mut() router.navigating = true;
.push(Route::Reply(NoteId::new(replying_to.to_owned())));
column.navigating = true;
None None
} }
BarAction::OpenThread => { BarAction::OpenThread(note_id) => {
open_thread(ndb, txn, column, note_cache, pool, threads, replying_to) open_thread(ndb, txn, router, note_cache, pool, threads, note_id.bytes())
} }
} }
} }
/// Execute the BarAction and process the BarResult
pub fn execute_and_process_result(
self,
ndb: &Ndb,
router: &mut Router<Route>,
threads: &mut Threads,
note_cache: &mut NoteCache,
pool: &mut RelayPool,
txn: &Transaction,
) {
if let Some(br) = self.execute(ndb, router, threads, note_cache, pool, txn) {
br.process(ndb, txn, threads);
}
}
} }
impl BarResult { impl BarResult {
pub fn new_thread_notes(notes: Vec<NoteRef>, root_id: NoteId) -> Self { pub fn new_thread_notes(notes: Vec<NoteRef>, root_id: NoteId) -> Self {
BarResult::NewThreadNotes(NewThreadNotes::new(notes, root_id)) BarResult::NewThreadNotes(NewThreadNotes::new(notes, root_id))
} }
pub fn process(&self, ndb: &Ndb, txn: &Transaction, threads: &mut Threads) {
match self {
// update the thread for next render if we have new notes
BarResult::NewThreadNotes(new_notes) => {
let thread = threads
.thread_mut(ndb, txn, new_notes.root_id.bytes())
.get_ptr();
new_notes.process(thread);
}
}
}
} }
impl NewThreadNotes { impl NewThreadNotes {
@@ -159,6 +180,6 @@ impl NewThreadNotes {
pub fn process(&self, thread: &mut Thread) { pub fn process(&self, thread: &mut Thread) {
// threads are chronological, ie reversed from reverse-chronological, the default. // threads are chronological, ie reversed from reverse-chronological, the default.
let reversed = true; let reversed = true;
thread.view.insert(&self.notes, reversed); thread.view_mut().insert(&self.notes, reversed);
} }
} }

View File

@@ -1,30 +1,28 @@
use crate::account_manager::AccountManager; use crate::{
use crate::actionbar::BarResult; account_manager::AccountManager,
use crate::app_creation::setup_cc; app_creation::setup_cc,
use crate::app_style::user_requested_visuals_change; app_style::user_requested_visuals_change,
use crate::args::Args; args::Args,
use crate::column::{Column, ColumnKind, Columns}; column::Columns,
use crate::draft::Drafts; draft::Drafts,
use crate::error::{Error, FilterError}; error::{Error, FilterError},
use crate::filter::FilterState; filter,
use crate::frame_history::FrameHistory; filter::FilterState,
use crate::imgcache::ImageCache; frame_history::FrameHistory,
use crate::key_storage::KeyStorageType; imgcache::ImageCache,
use crate::login_manager::LoginState; key_storage::KeyStorageType,
use crate::note::NoteRef; nav,
use crate::notecache::{CachedNote, NoteCache}; note::NoteRef,
use crate::relay_pool_manager::RelayPoolManager; notecache::{CachedNote, NoteCache},
use crate::routable_widget_state::RoutableWidgetState; subscriptions::{SubKind, Subscriptions},
use crate::route::{ManageAccountRoute, Route}; thread::Threads,
use crate::subscriptions::{SubKind, Subscriptions}; timeline::{Timeline, TimelineKind, ViewFilter},
use crate::thread::{DecrementResult, Threads}; ui::{self, AccountSelectionWidget, DesktopSidePanel},
use crate::timeline::{Timeline, TimelineKind, TimelineSource, ViewFilter}; unknowns::UnknownIds,
use crate::ui::note::PostAction; view_state::ViewState,
use crate::ui::{self, AccountSelectionWidget}; Result,
use crate::ui::{DesktopSidePanel, RelayView, View}; };
use crate::unknowns::UnknownIds;
use crate::{filter, Result};
use egui_nav::{Nav, NavAction};
use enostr::{ClientMessage, RelayEvent, RelayMessage, RelayPool}; use enostr::{ClientMessage, RelayEvent, RelayMessage, RelayPool};
use uuid::Uuid; use uuid::Uuid;
@@ -47,18 +45,17 @@ pub enum DamusState {
/// We derive Deserialize/Serialize so we can persist app state on shutdown. /// We derive Deserialize/Serialize so we can persist app state on shutdown.
pub struct Damus { pub struct Damus {
state: DamusState, state: DamusState,
note_cache: NoteCache, pub note_cache: NoteCache,
pub pool: RelayPool, pub pool: RelayPool,
pub columns: Columns, pub columns: Columns,
pub account_management_view_state: RoutableWidgetState<ManageAccountRoute>,
pub ndb: Ndb, pub ndb: Ndb,
pub view_state: ViewState,
pub unknown_ids: UnknownIds, pub unknown_ids: UnknownIds,
pub drafts: Drafts, pub drafts: Drafts,
pub threads: Threads, pub threads: Threads,
pub img_cache: ImageCache, pub img_cache: ImageCache,
pub accounts: AccountManager, pub accounts: AccountManager,
pub login_state: LoginState,
pub subscriptions: Subscriptions, pub subscriptions: Subscriptions,
frame_history: crate::frame_history::FrameHistory, frame_history: crate::frame_history::FrameHistory,
@@ -267,24 +264,24 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> {
} }
} }
let n_cols = damus.columns.columns().len(); let n_timelines = damus.columns.timelines().len();
for col_ind in 0..n_cols { for timeline_ind in 0..n_timelines {
let timeline = let is_ready = {
if let ColumnKind::Timeline(timeline) = damus.columns.column_mut(col_ind).kind_mut() { let timeline = &mut damus.columns.timelines[timeline_ind];
timeline matches!(
} else { is_timeline_ready(&damus.ndb, &mut damus.pool, &mut damus.note_cache, timeline),
continue; Ok(true)
}; )
};
if let Ok(true) = if is_ready {
is_timeline_ready(&damus.ndb, &mut damus.pool, &mut damus.note_cache, timeline)
{
let txn = Transaction::new(&damus.ndb).expect("txn"); let txn = Transaction::new(&damus.ndb).expect("txn");
if let Err(err) = TimelineSource::column(timeline.id).poll_notes_into_view(
&txn, if let Err(err) = Timeline::poll_notes_into_view(
timeline_ind,
&mut damus.columns.timelines,
&damus.ndb, &damus.ndb,
&mut damus.columns, &txn,
&mut damus.threads,
&mut damus.unknown_ids, &mut damus.unknown_ids,
&mut damus.note_cache, &mut damus.note_cache,
) { ) {
@@ -667,21 +664,21 @@ impl Damus {
.map(|a| a.pubkey.bytes()); .map(|a| a.pubkey.bytes());
let ndb = Ndb::new(&dbpath, &config).expect("ndb"); let ndb = Ndb::new(&dbpath, &config).expect("ndb");
let mut columns: Vec<Column> = Vec::with_capacity(parsed_args.columns.len()); let mut columns: Columns = Columns::new();
for col in parsed_args.columns { for col in parsed_args.columns {
if let Some(timeline) = col.into_timeline(&ndb, account) { if let Some(timeline) = col.into_timeline(&ndb, account) {
columns.push(Column::timeline(timeline)); columns.add_timeline(timeline);
} }
} }
let debug = parsed_args.debug; let debug = parsed_args.debug;
if columns.is_empty() { if columns.columns().is_empty() {
let filter = Filter::from_json(include_str!("../queries/timeline.json")).unwrap(); let filter = Filter::from_json(include_str!("../queries/timeline.json")).unwrap();
columns.push(Column::timeline(Timeline::new( columns.add_timeline(Timeline::new(
TimelineKind::Generic, TimelineKind::Generic,
FilterState::ready(vec![filter]), FilterState::ready(vec![filter]),
))); ))
} }
Self { Self {
@@ -695,17 +692,52 @@ impl Damus {
state: DamusState::Initializing, state: DamusState::Initializing,
img_cache: ImageCache::new(imgcache_dir.into()), img_cache: ImageCache::new(imgcache_dir.into()),
note_cache: NoteCache::default(), note_cache: NoteCache::default(),
columns: Columns::new(columns), columns,
textmode: parsed_args.textmode, textmode: parsed_args.textmode,
ndb, ndb,
accounts, accounts,
frame_history: FrameHistory::default(), frame_history: FrameHistory::default(),
show_account_switcher: false, show_account_switcher: false,
account_management_view_state: RoutableWidgetState::default(), view_state: ViewState::default(),
login_state: LoginState::default(),
} }
} }
pub fn pool_mut(&mut self) -> &mut RelayPool {
&mut self.pool
}
pub fn ndb(&self) -> &Ndb {
&self.ndb
}
pub fn drafts_mut(&mut self) -> &mut Drafts {
&mut self.drafts
}
pub fn img_cache_mut(&mut self) -> &mut ImageCache {
&mut self.img_cache
}
pub fn accounts(&self) -> &AccountManager {
&self.accounts
}
pub fn accounts_mut(&mut self) -> &mut AccountManager {
&mut self.accounts
}
pub fn view_state_mut(&mut self) -> &mut ViewState {
&mut self.view_state
}
pub fn columns_mut(&mut self) -> &mut Columns {
&mut self.columns
}
pub fn columns(&self) -> &Columns {
&self.columns
}
pub fn gen_subid(&self, kind: &SubKind) -> String { pub fn gen_subid(&self, kind: &SubKind) -> String {
if self.debug { if self.debug {
format!("{:?}", kind) format!("{:?}", kind)
@@ -715,12 +747,12 @@ impl Damus {
} }
pub fn mock<P: AsRef<Path>>(data_path: P) -> Self { pub fn mock<P: AsRef<Path>>(data_path: P) -> Self {
let mut columns: Vec<Column> = vec![]; let mut columns = Columns::new();
let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap(); let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap();
columns.push(Column::timeline(Timeline::new(
TimelineKind::Universe, let timeline = Timeline::new(TimelineKind::Universe, FilterState::ready(vec![filter]));
FilterState::ready(vec![filter]),
))); columns.add_timeline(timeline);
let imgcache_dir = data_path.as_ref().join(ImageCache::rel_datadir()); let imgcache_dir = data_path.as_ref().join(ImageCache::rel_datadir());
let _ = std::fs::create_dir_all(imgcache_dir.clone()); let _ = std::fs::create_dir_all(imgcache_dir.clone());
@@ -739,14 +771,13 @@ impl Damus {
pool: RelayPool::new(), pool: RelayPool::new(),
img_cache: ImageCache::new(imgcache_dir), img_cache: ImageCache::new(imgcache_dir),
note_cache: NoteCache::default(), note_cache: NoteCache::default(),
columns: Columns::new(columns), columns,
textmode: false, textmode: false,
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"), ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
accounts: AccountManager::new(None, KeyStorageType::None), accounts: AccountManager::new(None, KeyStorageType::None),
frame_history: FrameHistory::default(), frame_history: FrameHistory::default(),
show_account_switcher: false, show_account_switcher: false,
account_management_view_state: RoutableWidgetState::default(), view_state: ViewState::default(),
login_state: LoginState::default(),
} }
} }
@@ -758,6 +789,18 @@ impl Damus {
&mut self.note_cache &mut self.note_cache
} }
pub fn unknown_ids_mut(&mut self) -> &mut UnknownIds {
&mut self.unknown_ids
}
pub fn threads(&self) -> &Threads {
&self.threads
}
pub fn threads_mut(&mut self) -> &mut Threads {
&mut self.threads
}
pub fn note_cache(&self) -> &NoteCache { pub fn note_cache(&self) -> &NoteCache {
&self.note_cache &self.note_cache
} }
@@ -852,211 +895,6 @@ fn render_panel(ctx: &egui::Context, app: &mut Damus) {
}); });
} }
/// Local thread unsubscribe
fn thread_unsubscribe(
ndb: &Ndb,
threads: &mut Threads,
pool: &mut RelayPool,
note_cache: &mut NoteCache,
id: &[u8; 32],
) {
let (unsubscribe, remote_subid) = {
let txn = Transaction::new(ndb).expect("txn");
let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, &txn, id);
let thread = threads.thread_mut(ndb, &txn, root_id).get_ptr();
let unsub = thread.decrement_sub();
let mut remote_subid: Option<String> = None;
if let Ok(DecrementResult::LastSubscriber(_subid)) = unsub {
*thread.subscription_mut() = None;
remote_subid = thread.remote_subscription().to_owned();
*thread.remote_subscription_mut() = None;
}
(unsub, remote_subid)
};
match unsubscribe {
Ok(DecrementResult::LastSubscriber(sub)) => {
if let Err(e) = ndb.unsubscribe(sub) {
error!(
"failed to unsubscribe from thread: {e}, subid:{}, {} active subscriptions",
sub.id(),
ndb.subscription_count()
);
} else {
info!(
"Unsubscribed from thread subid:{}. {} active subscriptions",
sub.id(),
ndb.subscription_count()
);
}
// unsub from remote
if let Some(subid) = remote_subid {
pool.unsubscribe(subid);
}
}
Ok(DecrementResult::ActiveSubscribers) => {
info!(
"Keeping thread subscription. {} active subscriptions.",
ndb.subscription_count()
);
// do nothing
}
Err(e) => {
// something is wrong!
error!(
"Thread unsubscribe error: {e}. {} active subsciptions.",
ndb.subscription_count()
);
}
}
}
fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui::Ui) {
let navigating = app.columns.column(col).navigating;
let returning = app.columns.column(col).returning;
let nav_response = Nav::new(app.columns.column(col).routes().to_vec())
.navigating(navigating)
.returning(returning)
.title(false)
.show_mut(ui, |ui, nav| match nav.top() {
Route::Timeline(_n) => {
let column = app.columns.column_mut(col);
if column.kind().timeline().is_some() {
if show_postbox {
if let Some(kp) = app.accounts.selected_or_first_nsec() {
ui::timeline::postbox_view(
&app.ndb,
kp,
&mut app.pool,
&mut app.drafts,
&mut app.img_cache,
ui,
);
}
}
ui::TimelineView::new(
&app.ndb,
column,
&mut app.note_cache,
&mut app.img_cache,
&mut app.threads,
&mut app.pool,
app.textmode,
)
.ui(ui);
} else {
ui.label("no timeline for this column?");
}
None
}
Route::Relays => {
let manager = RelayPoolManager::new(&mut app.pool);
RelayView::new(manager).ui(ui);
None
}
Route::Thread(id) => {
let result = ui::ThreadView::new(
col,
&mut app.columns,
&mut app.threads,
&app.ndb,
&mut app.note_cache,
&mut app.img_cache,
&mut app.unknown_ids,
&mut app.pool,
app.textmode,
id.bytes(),
)
.ui(ui);
if let Some(bar_result) = result {
match bar_result {
BarResult::NewThreadNotes(new_notes) => {
let thread = app.threads.thread_expected_mut(new_notes.root_id.bytes());
new_notes.process(thread);
}
}
}
None
}
Route::Reply(id) => {
let txn = if let Ok(txn) = Transaction::new(&app.ndb) {
txn
} else {
ui.label("Reply to unknown note");
return None;
};
let note = if let Ok(note) = app.ndb.get_note_by_id(&txn, id.bytes()) {
note
} else {
ui.label("Reply to unknown note");
return None;
};
let id = egui::Id::new((
"post",
app.columns.column(col).view_id(),
note.key().unwrap(),
));
if let Some(poster) = app.accounts.selected_or_first_nsec() {
let response = egui::ScrollArea::vertical().show(ui, |ui| {
ui::PostReplyView::new(
&app.ndb,
poster,
&mut app.pool,
&mut app.drafts,
&mut app.note_cache,
&mut app.img_cache,
&note,
)
.id_source(id)
.show(ui)
});
Some(response)
} else {
None
}
}
});
let column = app.columns.column_mut(col);
if let Some(reply_response) = nav_response.inner {
if let Some(PostAction::Post(_np)) = reply_response.inner.action {
column.returning = true;
}
}
if let Some(NavAction::Returned) = nav_response.action {
let popped = column.routes_mut().pop();
if let Some(Route::Thread(id)) = popped {
thread_unsubscribe(
&app.ndb,
&mut app.threads,
&mut app.pool,
&mut app.note_cache,
id.bytes(),
);
}
column.returning = false;
} else if let Some(NavAction::Navigated) = nav_response.action {
column.navigating = false;
}
}
fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) {
//render_panel(ctx, app, 0); //render_panel(ctx, app, 0);
@@ -1067,7 +905,7 @@ fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) {
main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| {
if !app.columns.columns().is_empty() { if !app.columns.columns().is_empty() {
render_nav(false, 0, app, ui); nav::render_nav(false, 0, app, ui);
} }
}); });
} }
@@ -1143,12 +981,20 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usiz
for column_ind in 0..n_cols { for column_ind in 0..n_cols {
strip.cell(|ui| { strip.cell(|ui| {
let rect = ui.available_rect_before_wrap(); let rect = ui.available_rect_before_wrap();
let show_postbox = let show_postbox = first
first && app.columns.column(column_ind).kind().timeline().is_some(); && app
.columns
.column(column_ind)
.router()
.routes()
.iter()
.find_map(|r| r.timeline_id())
.is_some();
if show_postbox { if show_postbox {
first = false first = false
} }
render_nav(show_postbox, column_ind, app, ui);
nav::render_nav(show_postbox, column_ind, app, ui);
// vertical line // vertical line
ui.painter().vline( ui.painter().vline(

View File

@@ -1,100 +1,86 @@
use crate::route::Route; use crate::route::{Route, Router};
use crate::timeline::{Timeline, TimelineId}; use crate::timeline::{Timeline, TimelineId};
use std::iter::Iterator; use std::iter::Iterator;
use tracing::warn; use tracing::warn;
pub struct Column { pub struct Column {
kind: ColumnKind, router: Router<Route>,
routes: Vec<Route>,
pub navigating: bool,
pub returning: bool,
} }
impl Column { impl Column {
pub fn timeline(timeline: Timeline) -> Self { pub fn new(routes: Vec<Route>) -> Self {
let routes = vec![Route::Timeline(format!("{}", &timeline.kind))]; let router = Router::new(routes);
let kind = ColumnKind::Timeline(timeline); Column { router }
Column::new(kind, routes)
} }
pub fn kind(&self) -> &ColumnKind { pub fn router(&self) -> &Router<Route> {
&self.kind &self.router
} }
pub fn kind_mut(&mut self) -> &mut ColumnKind { pub fn router_mut(&mut self) -> &mut Router<Route> {
&mut self.kind &mut self.router
}
pub fn view_id(&self) -> egui::Id {
self.kind.view_id()
}
pub fn routes(&self) -> &[Route] {
&self.routes
}
pub fn routes_mut(&mut self) -> &mut Vec<Route> {
&mut self.routes
}
pub fn new(kind: ColumnKind, routes: Vec<Route>) -> Self {
let navigating = false;
let returning = false;
Column {
kind,
routes,
navigating,
returning,
}
} }
} }
#[derive(Default)]
pub struct Columns { pub struct Columns {
/// Columns are simply routers into settings, timelines, etc
columns: Vec<Column>, columns: Vec<Column>,
/// Timeline state is not tied to routing logic separately, so that
/// different columns can navigate to and from settings to timelines,
/// etc.
pub timelines: Vec<Timeline>,
/// The selected column for key navigation /// The selected column for key navigation
selected: i32, selected: i32,
} }
impl Columns { impl Columns {
pub fn new() -> Self {
Columns::default()
}
pub fn add_timeline(&mut self, timeline: Timeline) {
let routes = vec![Route::timeline(timeline.id)];
self.timelines.push(timeline);
self.columns.push(Column::new(routes))
}
pub fn columns_mut(&mut self) -> &mut Vec<Column> { pub fn columns_mut(&mut self) -> &mut Vec<Column> {
&mut self.columns &mut self.columns
} }
pub fn timeline_mut(&mut self, timeline_ind: usize) -> &mut Timeline {
&mut self.timelines[timeline_ind]
}
pub fn column(&self, ind: usize) -> &Column { pub fn column(&self, ind: usize) -> &Column {
&self.columns()[ind] &self.columns()[ind]
} }
pub fn columns(&self) -> &[Column] { pub fn columns(&self) -> &Vec<Column> {
&self.columns &self.columns
} }
pub fn new(columns: Vec<Column>) -> Self {
let selected = 0;
Columns { columns, selected }
}
pub fn selected(&mut self) -> &mut Column { pub fn selected(&mut self) -> &mut Column {
&mut self.columns[self.selected as usize] &mut self.columns[self.selected as usize]
} }
pub fn timelines_mut(&mut self) -> impl Iterator<Item = &mut Timeline> { pub fn timelines_mut(&mut self) -> &mut Vec<Timeline> {
self.columns &mut self.timelines
.iter_mut()
.filter_map(|c| c.kind_mut().timeline_mut())
} }
pub fn timelines(&self) -> impl Iterator<Item = &Timeline> { pub fn timelines(&self) -> &Vec<Timeline> {
self.columns.iter().filter_map(|c| c.kind().timeline()) &self.timelines
} }
pub fn find_timeline_mut(&mut self, id: TimelineId) -> Option<&mut Timeline> { pub fn find_timeline_mut(&mut self, id: TimelineId) -> Option<&mut Timeline> {
self.timelines_mut().find(|tl| tl.id == id) self.timelines_mut().iter_mut().find(|tl| tl.id == id)
} }
pub fn find_timeline(&self, id: TimelineId) -> Option<&Timeline> { pub fn find_timeline(&self, id: TimelineId) -> Option<&Timeline> {
self.timelines().find(|tl| tl.id == id) self.timelines().iter().find(|tl| tl.id == id)
} }
pub fn column_mut(&mut self, ind: usize) -> &mut Column { pub fn column_mut(&mut self, ind: usize) -> &mut Column {
@@ -102,11 +88,11 @@ impl Columns {
} }
pub fn select_down(&mut self) { pub fn select_down(&mut self) {
self.selected().kind_mut().select_down(); warn!("todo: implement select_down");
} }
pub fn select_up(&mut self) { pub fn select_up(&mut self) {
self.selected().kind_mut().select_up(); warn!("todo: implement select_up");
} }
pub fn select_left(&mut self) { pub fn select_left(&mut self) {
@@ -123,48 +109,3 @@ impl Columns {
self.selected += 1; self.selected += 1;
} }
} }
/// What type of column is it?
#[derive(Debug)]
pub enum ColumnKind {
Timeline(Timeline),
ManageAccount,
}
impl ColumnKind {
pub fn timeline_mut(&mut self) -> Option<&mut Timeline> {
match self {
ColumnKind::Timeline(tl) => Some(tl),
_ => None,
}
}
pub fn timeline(&self) -> Option<&Timeline> {
match self {
ColumnKind::Timeline(tl) => Some(tl),
_ => None,
}
}
pub fn view_id(&self) -> egui::Id {
match self {
ColumnKind::Timeline(timeline) => timeline.view_id(),
ColumnKind::ManageAccount => egui::Id::new("manage_account"),
}
}
pub fn select_down(&mut self) {
match self {
ColumnKind::Timeline(tl) => tl.current_view_mut().select_down(),
ColumnKind::ManageAccount => warn!("todo: manage account select_down"),
}
}
pub fn select_up(&mut self) {
match self {
ColumnKind::Timeline(tl) => tl.current_view_mut().select_down(),
ColumnKind::ManageAccount => warn!("todo: manage account select_down"),
}
}
}

View File

@@ -40,9 +40,10 @@ impl fmt::Display for SubscriptionError {
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
TimelineNotFound,
LoadFailed,
SubscriptionError(SubscriptionError), SubscriptionError(SubscriptionError),
Filter(FilterError), Filter(FilterError),
LoadFailed,
Io(io::Error), Io(io::Error),
Nostr(enostr::Error), Nostr(enostr::Error),
Ndb(nostrdb::Error), Ndb(nostrdb::Error),
@@ -72,6 +73,7 @@ impl fmt::Display for Error {
Self::SubscriptionError(e) => { Self::SubscriptionError(e) => {
write!(f, "{e}") write!(f, "{e}")
} }
Self::TimelineNotFound => write!(f, "Timeline not found"),
Self::LoadFailed => { Self::LoadFailed => {
write!(f, "load failed") write!(f, "load failed")
} }

View File

@@ -21,13 +21,13 @@ mod key_parsing;
mod key_storage; mod key_storage;
pub mod login_manager; pub mod login_manager;
mod macos_key_storage; mod macos_key_storage;
mod nav;
mod note; mod note;
mod notecache; mod notecache;
mod post; mod post;
mod profile; mod profile;
pub mod relay_pool_manager; pub mod relay_pool_manager;
mod result; mod result;
mod routable_widget_state;
mod route; mod route;
mod subscriptions; mod subscriptions;
mod test_data; mod test_data;
@@ -38,6 +38,7 @@ mod timeline;
pub mod ui; pub mod ui;
mod unknowns; mod unknowns;
mod user_account; mod user_account;
mod view_state;
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]

View File

@@ -16,13 +16,7 @@ pub struct LoginState {
impl<'a> LoginState { impl<'a> LoginState {
pub fn new() -> Self { pub fn new() -> Self {
LoginState { LoginState::default()
login_key: String::new(),
promise_query: None,
error: None,
key_on_error: None,
should_create_new: false,
}
} }
/// Get the textedit for the login UI without exposing the key variable /// Get the textedit for the login UI without exposing the key variable

84
src/nav.rs Normal file
View File

@@ -0,0 +1,84 @@
use crate::{
account_manager::render_accounts_route,
relay_pool_manager::RelayPoolManager,
route::Route,
thread::thread_unsubscribe,
timeline::route::{render_timeline_route, TimelineRoute, TimelineRouteResponse},
ui::{note::PostAction, RelayView, View},
Damus,
};
use egui_nav::{Nav, NavAction};
pub fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui::Ui) {
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)
.title(false)
.show_mut(ui, |ui, nav| match nav.top() {
Route::Timeline(tlr) => render_timeline_route(
&app.ndb,
&mut app.columns,
&mut app.pool,
&mut app.drafts,
&mut app.img_cache,
&mut app.note_cache,
&mut app.threads,
&mut app.accounts,
*tlr,
col,
show_postbox,
app.textmode,
ui,
),
Route::Accounts(amr) => {
render_accounts_route(
ui,
&app.ndb,
col,
&mut app.columns,
&mut app.img_cache,
&mut app.accounts,
&mut app.view_state.login,
*amr,
);
None
}
Route::Relays => {
let manager = RelayPoolManager::new(app.pool_mut());
RelayView::new(manager).ui(ui);
None
}
});
if let Some(reply_response) = nav_response.inner {
// start returning when we're finished posting
match reply_response {
TimelineRouteResponse::Post(resp) => {
if let Some(action) = resp.action {
match action {
PostAction::Post(_) => {
app.columns_mut().column_mut(col).router_mut().returning = true;
}
}
}
}
}
}
if let Some(NavAction::Returned) = nav_response.action {
let r = app.columns_mut().column_mut(col).router_mut().go_back();
if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r {
thread_unsubscribe(
&app.ndb,
&mut app.threads,
&mut app.pool,
&mut app.note_cache,
id.bytes(),
);
}
app.columns_mut().column_mut(col).router_mut().returning = false;
} else if let Some(NavAction::Navigated) = nav_response.action {
app.columns_mut().column_mut(col).router_mut().navigating = false;
}
}

View File

@@ -1,5 +1,5 @@
use crate::notecache::NoteCache; use crate::notecache::NoteCache;
use nostrdb::{Ndb, NoteKey, QueryResult, Transaction}; use nostrdb::{Ndb, Note, NoteKey, QueryResult, Transaction};
use std::cmp::Ordering; use std::cmp::Ordering;
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
@@ -13,6 +13,12 @@ impl NoteRef {
NoteRef { key, created_at } NoteRef { key, created_at }
} }
pub fn from_note(note: &Note<'_>) -> Self {
let created_at = note.created_at();
let key = note.key().expect("todo: implement NoteBuf");
NoteRef::new(key, created_at)
}
pub fn from_query_result(qr: QueryResult<'_>) -> Self { pub fn from_query_result(qr: QueryResult<'_>) -> Self {
NoteRef { NoteRef {
key: qr.note_key, key: qr.note_key,

View File

@@ -1,26 +0,0 @@
#[derive(Default)]
pub struct RoutableWidgetState<R: Clone> {
routes: Vec<R>,
}
impl<R: Clone> RoutableWidgetState<R> {
pub fn route_to(&mut self, route: R) {
self.routes.push(route);
}
pub fn clear(&mut self) {
self.routes.clear();
}
pub fn go_back(&mut self) {
self.routes.pop();
}
pub fn top(&self) -> Option<R> {
self.routes.last().cloned()
}
pub fn get_routes(&self) -> Vec<R> {
self.routes.clone()
}
}

View File

@@ -1,51 +1,107 @@
use egui::RichText;
use enostr::NoteId; use enostr::NoteId;
use std::fmt::{self}; use std::fmt::{self};
use strum_macros::Display;
use crate::ui::{ use crate::{
account_login_view::AccountLoginResponse, account_management::AccountManagementViewResponse, account_manager::AccountsRoute,
timeline::{TimelineId, TimelineRoute},
}; };
/// App routing. These describe different places you can go inside Notedeck. /// App routing. These describe different places you can go inside Notedeck.
#[derive(Clone, Debug)] #[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum Route { pub enum Route {
Timeline(String), Timeline(TimelineRoute),
Thread(NoteId), Accounts(AccountsRoute),
Reply(NoteId),
Relays, Relays,
} }
#[derive(Clone, Debug, Default, Display)] impl Route {
pub enum ManageAccountRoute { pub fn timeline(timeline_id: TimelineId) -> Self {
#[default] Route::Timeline(TimelineRoute::Timeline(timeline_id))
AccountManagement, }
AddAccount,
pub fn timeline_id(&self) -> Option<&TimelineId> {
if let Route::Timeline(TimelineRoute::Timeline(tid)) = self {
Some(tid)
} else {
None
}
}
pub fn thread(thread_root: NoteId) -> Self {
Route::Timeline(TimelineRoute::Thread(thread_root))
}
pub fn reply(replying_to: NoteId) -> Self {
Route::Timeline(TimelineRoute::Reply(replying_to))
}
pub fn accounts() -> Self {
Route::Accounts(AccountsRoute::Accounts)
}
pub fn add_account() -> Self {
Route::Accounts(AccountsRoute::AddAccount)
}
}
// TODO: add this to egui-nav so we don't have to deal with returning
// and navigating headaches
#[derive(Clone)]
pub struct Router<R: Clone> {
routes: Vec<R>,
pub returning: bool,
pub navigating: bool,
}
impl<R: Clone> Router<R> {
pub fn new(routes: Vec<R>) -> Self {
if routes.is_empty() {
panic!("routes can't be empty")
}
let returning = false;
let navigating = false;
Router {
routes,
returning,
navigating,
}
}
pub fn route_to(&mut self, route: R) {
self.routes.push(route);
}
pub fn go_back(&mut self) -> Option<R> {
if self.routes.len() == 1 {
return None;
}
self.routes.pop()
}
pub fn top(&self) -> &R {
self.routes.last().expect("routes can't be empty")
}
pub fn routes(&self) -> &Vec<R> {
&self.routes
}
} }
impl fmt::Display for Route { impl fmt::Display for Route {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Route::Timeline(name) => write!(f, "{}", name), Route::Timeline(tlr) => match tlr {
Route::Thread(_id) => write!(f, "Thread"), TimelineRoute::Timeline(name) => write!(f, "{}", name),
Route::Reply(_id) => write!(f, "Reply"), TimelineRoute::Thread(_id) => write!(f, "Thread"),
TimelineRoute::Reply(_id) => write!(f, "Reply"),
},
Route::Relays => write!(f, "Relays"), Route::Relays => write!(f, "Relays"),
Route::Accounts(amr) => match amr {
AccountsRoute::Accounts => write!(f, "Accounts"),
AccountsRoute::AddAccount => write!(f, "Add Account"),
},
} }
} }
} }
impl Route {
pub fn title(&self) -> RichText {
match self {
Route::Thread(_) => RichText::new("Thread"),
Route::Reply(_) => RichText::new("Reply"),
Route::Relays => RichText::new("Relays"),
Route::Timeline(_) => RichText::new("Timeline"),
}
}
}
pub enum ManageAcountRouteResponse {
AccountManagement(AccountManagementViewResponse),
AddAccount(AccountLoginResponse),
}

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use enostr::{FullKeypair, Pubkey, RelayPool}; use enostr::{FullKeypair, Pubkey, RelayPool};
use nostrdb::ProfileRecord; use nostrdb::ProfileRecord;
use crate::{account_manager::UserAccount, Damus}; use crate::{user_account::UserAccount, Damus};
#[allow(unused_must_use)] #[allow(unused_must_use)]
pub fn sample_pool() -> RelayPool { pub fn sample_pool() -> RelayPool {
@@ -101,7 +101,7 @@ pub fn test_app() -> Damus {
let accounts = get_test_accounts(); let accounts = get_test_accounts();
for account in accounts { for account in accounts {
app.accounts.add_account(account); app.accounts_mut().add_account(account);
} }
app app

View File

@@ -1,14 +1,18 @@
use crate::note::NoteRef; use crate::{
use crate::timeline::{TimelineTab, ViewFilter}; note::NoteRef,
use crate::Error; notecache::NoteCache,
use nostrdb::{Filter, FilterBuilder, Ndb, Subscription, Transaction}; timeline::{TimelineTab, ViewFilter},
Error, Result,
};
use enostr::RelayPool;
use nostrdb::{Filter, FilterBuilder, Ndb, Note, Subscription, Transaction};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::HashMap; use std::collections::HashMap;
use tracing::{debug, warn}; use tracing::{debug, error, info, warn};
#[derive(Default)] #[derive(Default)]
pub struct Thread { pub struct Thread {
pub view: TimelineTab, view: TimelineTab,
sub: Option<Subscription>, sub: Option<Subscription>,
remote_sub: Option<String>, remote_sub: Option<String>,
pub subscribers: i32, pub subscribers: i32,
@@ -40,6 +44,48 @@ impl Thread {
} }
} }
pub fn view(&self) -> &TimelineTab {
&self.view
}
pub fn view_mut(&mut self) -> &mut TimelineTab {
&mut self.view
}
#[must_use = "UnknownIds::update_from_note_refs should be used on this result"]
pub fn poll_notes_into_view<'a>(
&mut self,
txn: &'a Transaction,
ndb: &Ndb,
) -> Result<Vec<Note<'a>>> {
let sub = self.subscription().expect("thread subscription");
let new_note_keys = ndb.poll_for_notes(sub, 500);
if new_note_keys.is_empty() {
return Ok(vec![]);
} else {
debug!("{} new notes! {:?}", new_note_keys.len(), new_note_keys);
}
let mut notes: Vec<Note<'a>> = Vec::with_capacity(new_note_keys.len());
for key in new_note_keys {
let note = if let Ok(note) = ndb.get_note_by_key(txn, key) {
note
} else {
continue;
};
notes.push(note);
}
{
let reversed = true;
let note_refs: Vec<NoteRef> = notes.iter().map(|n| NoteRef::from_note(n)).collect();
self.view.insert(&note_refs, reversed);
}
Ok(notes)
}
/// Look for new thread notes since our last fetch /// Look for new thread notes since our last fetch
pub fn new_notes( pub fn new_notes(
notes: &[NoteRef], notes: &[NoteRef],
@@ -66,7 +112,7 @@ impl Thread {
} }
} }
pub fn decrement_sub(&mut self) -> Result<DecrementResult, Error> { pub fn decrement_sub(&mut self) -> Result<DecrementResult> {
self.subscribers -= 1; self.subscribers -= 1;
match self.subscribers.cmp(&0) { match self.subscribers.cmp(&0) {
@@ -165,7 +211,7 @@ impl Threads {
// also use hashbrown? // also use hashbrown?
if self.root_id_to_thread.contains_key(root_id) { if self.root_id_to_thread.contains_key(root_id) {
return ThreadResult::Stale(self.root_id_to_thread.get_mut(root_id).unwrap()); return ThreadResult::Stale(self.thread_expected_mut(root_id));
} }
// we don't have the thread, query for it! // we don't have the thread, query for it!
@@ -198,3 +244,68 @@ impl Threads {
//fn thread_by_id(&self, ndb: &Ndb, id: &[u8; 32]) -> &mut Thread { //fn thread_by_id(&self, ndb: &Ndb, id: &[u8; 32]) -> &mut Thread {
//} //}
} }
/// Local thread unsubscribe
pub fn thread_unsubscribe(
ndb: &Ndb,
threads: &mut Threads,
pool: &mut RelayPool,
note_cache: &mut NoteCache,
id: &[u8; 32],
) {
let (unsubscribe, remote_subid) = {
let txn = Transaction::new(ndb).expect("txn");
let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, &txn, id);
let thread = threads.thread_mut(ndb, &txn, root_id).get_ptr();
let unsub = thread.decrement_sub();
let mut remote_subid: Option<String> = None;
if let Ok(DecrementResult::LastSubscriber(_subid)) = unsub {
*thread.subscription_mut() = None;
remote_subid = thread.remote_subscription().to_owned();
*thread.remote_subscription_mut() = None;
}
(unsub, remote_subid)
};
match unsubscribe {
Ok(DecrementResult::LastSubscriber(sub)) => {
if let Err(e) = ndb.unsubscribe(sub) {
error!(
"failed to unsubscribe from thread: {e}, subid:{}, {} active subscriptions",
sub.id(),
ndb.subscription_count()
);
} else {
info!(
"Unsubscribed from thread subid:{}. {} active subscriptions",
sub.id(),
ndb.subscription_count()
);
}
// unsub from remote
if let Some(subid) = remote_subid {
pool.unsubscribe(subid);
}
}
Ok(DecrementResult::ActiveSubscribers) => {
info!(
"Keeping thread subscription. {} active subscriptions.",
ndb.subscription_count()
);
// do nothing
}
Err(e) => {
// something is wrong!
error!(
"Thread unsubscribe error: {e}. {} active subsciptions.",
ndb.subscription_count()
);
}
}
}

View File

@@ -1,8 +1,6 @@
use crate::column::Columns;
use crate::error::Error; use crate::error::Error;
use crate::note::NoteRef; use crate::note::NoteRef;
use crate::notecache::{CachedNote, NoteCache}; use crate::notecache::{CachedNote, NoteCache};
use crate::thread::Threads;
use crate::unknowns::UnknownIds; use crate::unknowns::UnknownIds;
use crate::Result; use crate::Result;
use crate::{filter, filter::FilterState}; use crate::{filter, filter::FilterState};
@@ -18,9 +16,11 @@ use std::rc::Rc;
use tracing::{debug, error}; use tracing::{debug, error};
mod kind; pub mod kind;
pub mod route;
pub use kind::{PubkeySource, TimelineKind}; pub use kind::{PubkeySource, TimelineKind};
pub use route::TimelineRoute;
#[derive(Debug, Hash, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Hash, Copy, Clone, Eq, PartialEq)]
pub struct TimelineId(u32); pub struct TimelineId(u32);
@@ -37,147 +37,6 @@ impl fmt::Display for TimelineId {
} }
} }
#[derive(Debug, Copy, Clone)]
pub enum TimelineSource<'a> {
Column(TimelineId),
Thread(&'a [u8; 32]),
}
impl<'a> TimelineSource<'a> {
pub fn column(id: TimelineId) -> Self {
TimelineSource::Column(id)
}
pub fn view<'b>(
self,
ndb: &Ndb,
columns: &'b mut Columns,
threads: &'b mut Threads,
txn: &Transaction,
filter: ViewFilter,
) -> &'b mut TimelineTab {
match self {
TimelineSource::Column(tid) => columns
.find_timeline_mut(tid)
.expect("timeline")
.view_mut(filter),
TimelineSource::Thread(root_id) => {
// TODO: replace all this with the raw entry api eventually
let thread = if threads.root_id_to_thread.contains_key(root_id) {
threads.thread_expected_mut(root_id)
} else {
threads.thread_mut(ndb, txn, root_id).get_ptr()
};
&mut thread.view
}
}
}
fn sub(
self,
ndb: &Ndb,
columns: &Columns,
txn: &Transaction,
threads: &mut Threads,
) -> Option<Subscription> {
match self {
TimelineSource::Column(tid) => columns.find_timeline(tid).expect("thread").subscription,
TimelineSource::Thread(root_id) => {
// TODO: replace all this with the raw entry api eventually
let thread = if threads.root_id_to_thread.contains_key(root_id) {
threads.thread_expected_mut(root_id)
} else {
threads.thread_mut(ndb, txn, root_id).get_ptr()
};
thread.subscription()
}
}
}
/// Check local subscriptions for new notes and insert them into
/// timelines (threads, columns)
pub fn poll_notes_into_view(
&self,
txn: &Transaction,
ndb: &Ndb,
columns: &mut Columns,
threads: &mut Threads,
unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache,
) -> Result<()> {
let sub = if let Some(sub) = self.sub(ndb, columns, txn, threads) {
sub
} else {
return Err(Error::no_active_sub());
};
let new_note_ids = ndb.poll_for_notes(sub, 100);
if new_note_ids.is_empty() {
return Ok(());
} else {
debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids);
}
let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len());
for key in new_note_ids {
let note = if let Ok(note) = ndb.get_note_by_key(txn, key) {
note
} else {
error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key);
continue;
};
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note);
let created_at = note.created_at();
new_refs.push((note, NoteRef { key, created_at }));
}
// We're assuming reverse-chronological here (timelines). This
// flag ensures we trigger the items_inserted_at_start
// optimization in VirtualList. We need this flag because we can
// insert notes into chronological order sometimes, and this
// optimization doesn't make sense in those situations.
let reversed = false;
// ViewFilter::NotesAndReplies
{
let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
let reversed = false;
self.view(ndb, columns, threads, txn, ViewFilter::NotesAndReplies)
.insert(&refs, reversed);
}
//
// handle the filtered case (ViewFilter::Notes, no replies)
//
// TODO(jb55): this is mostly just copied from above, let's just use a loop
// I initially tried this but ran into borrow checker issues
{
let mut filtered_refs = Vec::with_capacity(new_refs.len());
for (note, nr) in &new_refs {
let cached_note = note_cache.cached_note_or_insert(nr.key, note);
if ViewFilter::filter_notes(cached_note, note) {
filtered_refs.push(*nr);
}
}
self.view(ndb, columns, threads, txn, ViewFilter::Notes)
.insert(&filtered_refs, reversed);
}
Ok(())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub enum ViewFilter { pub enum ViewFilter {
Notes, Notes,
@@ -379,6 +238,80 @@ impl Timeline {
pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineTab { pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineTab {
&mut self.views[view.index()] &mut self.views[view.index()]
} }
pub fn poll_notes_into_view(
timeline_idx: usize,
timelines: &mut [Timeline],
ndb: &Ndb,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache,
) -> Result<()> {
let timeline = &mut timelines[timeline_idx];
let sub = timeline.subscription.ok_or(Error::no_active_sub())?;
let new_note_ids = ndb.poll_for_notes(sub, 500);
if new_note_ids.is_empty() {
return Ok(());
} else {
debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids);
}
let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len());
for key in new_note_ids {
let note = if let Ok(note) = ndb.get_note_by_key(txn, key) {
note
} else {
error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key);
continue;
};
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note);
let created_at = note.created_at();
new_refs.push((note, NoteRef { key, created_at }));
}
// We're assuming reverse-chronological here (timelines). This
// flag ensures we trigger the items_inserted_at_start
// optimization in VirtualList. We need this flag because we can
// insert notes into chronological order sometimes, and this
// optimization doesn't make sense in those situations.
let reversed = false;
// ViewFilter::NotesAndReplies
{
let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
let reversed = false;
timeline
.view_mut(ViewFilter::NotesAndReplies)
.insert(&refs, reversed);
}
//
// handle the filtered case (ViewFilter::Notes, no replies)
//
// TODO(jb55): this is mostly just copied from above, let's just use a loop
// I initially tried this but ran into borrow checker issues
{
let mut filtered_refs = Vec::with_capacity(new_refs.len());
for (note, nr) in &new_refs {
let cached_note = note_cache.cached_note_or_insert(nr.key, note);
if ViewFilter::filter_notes(cached_note, note) {
filtered_refs.push(*nr);
}
}
timeline
.view_mut(ViewFilter::Notes)
.insert(&filtered_refs, reversed);
}
Ok(())
}
} }
pub enum MergeKind { pub enum MergeKind {

113
src/timeline/route.rs Normal file
View File

@@ -0,0 +1,113 @@
use crate::{
account_manager::AccountManager,
column::Columns,
draft::Drafts,
imgcache::ImageCache,
notecache::NoteCache,
thread::Threads,
timeline::TimelineId,
ui::{self, note::post::PostResponse},
};
use enostr::{NoteId, RelayPool};
use nostrdb::{Ndb, Transaction};
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum TimelineRoute {
Timeline(TimelineId),
Thread(NoteId),
Reply(NoteId),
}
pub enum TimelineRouteResponse {
Post(PostResponse),
}
impl TimelineRouteResponse {
pub fn post(post: PostResponse) -> Self {
TimelineRouteResponse::Post(post)
}
}
#[allow(clippy::too_many_arguments)]
pub fn render_timeline_route(
ndb: &Ndb,
columns: &mut Columns,
pool: &mut RelayPool,
drafts: &mut Drafts,
img_cache: &mut ImageCache,
note_cache: &mut NoteCache,
threads: &mut Threads,
accounts: &mut AccountManager,
route: TimelineRoute,
col: usize,
show_postbox: bool,
textmode: bool,
ui: &mut egui::Ui,
) -> Option<TimelineRouteResponse> {
match route {
TimelineRoute::Timeline(timeline_id) => {
if show_postbox {
if let Some(kp) = accounts.selected_or_first_nsec() {
ui::timeline::postbox_view(ndb, kp, pool, drafts, img_cache, ui);
}
}
if let Some(bar_action) =
ui::TimelineView::new(timeline_id, columns, ndb, note_cache, img_cache, textmode)
.ui(ui)
{
let txn = Transaction::new(ndb).expect("txn");
let router = columns.columns_mut()[col].router_mut();
bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn);
}
None
}
TimelineRoute::Thread(id) => {
if let Some(bar_action) =
ui::ThreadView::new(threads, ndb, note_cache, img_cache, id.bytes(), textmode)
.id_source(egui::Id::new(("threadscroll", col)))
.ui(ui)
{
let txn = Transaction::new(ndb).expect("txn");
let router = columns.columns_mut()[col].router_mut();
bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn);
}
None
}
TimelineRoute::Reply(id) => {
let txn = if let Ok(txn) = Transaction::new(ndb) {
txn
} else {
ui.label("Reply to unknown note");
return None;
};
let note = if let Ok(note) = ndb.get_note_by_id(&txn, id.bytes()) {
note
} else {
ui.label("Reply to unknown note");
return None;
};
let id = egui::Id::new(("post", col, note.key().unwrap()));
if let Some(poster) = accounts.selected_or_first_nsec() {
let response = egui::ScrollArea::vertical().show(ui, |ui| {
ui::PostReplyView::new(ndb, poster, pool, drafts, note_cache, img_cache, &note)
.id_source(id)
.show(ui)
});
Some(TimelineRouteResponse::post(response.inner))
} else {
None
}
}
}
}

View File

@@ -2,6 +2,7 @@ use crate::colors::PINK;
use crate::imgcache::ImageCache; use crate::imgcache::ImageCache;
use crate::{ use crate::{
account_manager::AccountManager, account_manager::AccountManager,
route::{Route, Router},
ui::{Preview, PreviewConfig, View}, ui::{Preview, PreviewConfig, View},
Damus, Damus,
}; };
@@ -12,22 +13,29 @@ use super::profile::preview::SimpleProfilePreview;
use super::profile::ProfilePreviewOp; use super::profile::ProfilePreviewOp;
use super::profile_preview_controller::profile_preview_view; use super::profile_preview_controller::profile_preview_view;
pub struct AccountManagementView {} pub struct AccountsView<'a> {
ndb: &'a Ndb,
accounts: &'a AccountManager,
img_cache: &'a mut ImageCache,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum AccountManagementViewResponse { pub enum AccountsViewResponse {
SelectAccount(usize), SelectAccount(usize),
RemoveAccount(usize), RemoveAccount(usize),
RouteToLogin, RouteToLogin,
} }
impl AccountManagementView { impl<'a> AccountsView<'a> {
pub fn ui( pub fn new(ndb: &'a Ndb, accounts: &'a AccountManager, img_cache: &'a mut ImageCache) -> Self {
ui: &mut Ui, AccountsView {
account_manager: &AccountManager, ndb,
ndb: &Ndb, accounts,
img_cache: &mut ImageCache, img_cache,
) -> InnerResponse<Option<AccountManagementViewResponse>> { }
}
pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> {
Frame::none().outer_margin(12.0).show(ui, |ui| { Frame::none().outer_margin(12.0).show(ui, |ui| {
if let Some(resp) = Self::top_section_buttons_widget(ui).inner { if let Some(resp) = Self::top_section_buttons_widget(ui).inner {
return Some(resp); return Some(resp);
@@ -36,7 +44,7 @@ impl AccountManagementView {
ui.add_space(8.0); ui.add_space(8.0);
scroll_area() scroll_area()
.show(ui, |ui| { .show(ui, |ui| {
Self::show_accounts(ui, account_manager, ndb, img_cache) Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache)
}) })
.inner .inner
}) })
@@ -47,8 +55,8 @@ impl AccountManagementView {
account_manager: &AccountManager, account_manager: &AccountManager,
ndb: &Ndb, ndb: &Ndb,
img_cache: &mut ImageCache, img_cache: &mut ImageCache,
) -> Option<AccountManagementViewResponse> { ) -> Option<AccountsViewResponse> {
let mut return_op: Option<AccountManagementViewResponse> = None; let mut return_op: Option<AccountsViewResponse> = None;
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
Vec2::new(ui.available_size_before_wrap().x, 32.0), Vec2::new(ui.available_size_before_wrap().x, 32.0),
Layout::top_down(egui::Align::Min), Layout::top_down(egui::Align::Min),
@@ -82,11 +90,9 @@ impl AccountManagementView {
profile_preview_view(ui, profile.as_ref(), img_cache, is_selected) profile_preview_view(ui, profile.as_ref(), img_cache, is_selected)
{ {
return_op = Some(match op { return_op = Some(match op {
ProfilePreviewOp::SwitchTo => { ProfilePreviewOp::SwitchTo => AccountsViewResponse::SelectAccount(i),
AccountManagementViewResponse::SelectAccount(i)
}
ProfilePreviewOp::RemoveAccount => { ProfilePreviewOp::RemoveAccount => {
AccountManagementViewResponse::RemoveAccount(i) AccountsViewResponse::RemoveAccount(i)
} }
}); });
} }
@@ -98,21 +104,18 @@ impl AccountManagementView {
fn top_section_buttons_widget( fn top_section_buttons_widget(
ui: &mut egui::Ui, ui: &mut egui::Ui,
) -> InnerResponse<Option<AccountManagementViewResponse>> { ) -> InnerResponse<Option<AccountsViewResponse>> {
ui.horizontal(|ui| { ui.allocate_ui_with_layout(
ui.allocate_ui_with_layout( Vec2::new(ui.available_size_before_wrap().x, 32.0),
Vec2::new(ui.available_size_before_wrap().x, 32.0), Layout::left_to_right(egui::Align::Center),
Layout::left_to_right(egui::Align::Center), |ui| {
|ui| { if ui.add(add_account_button()).clicked() {
if ui.add(add_account_button()).clicked() { Some(AccountsViewResponse::RouteToLogin)
Some(AccountManagementViewResponse::RouteToLogin) } else {
} else { None
None }
} },
}, )
)
.inner
})
} }
} }
@@ -206,41 +209,41 @@ fn selected_widget() -> impl egui::Widget {
mod preview { mod preview {
use super::*; use super::*;
use crate::{account_manager::process_management_view_response_stateless, test_data}; use crate::{account_manager::process_accounts_view_response, test_data};
pub struct AccountManagementPreview { pub struct AccountsPreview {
app: Damus, app: Damus,
router: Router<Route>,
} }
impl AccountManagementPreview { impl AccountsPreview {
fn new() -> Self { fn new() -> Self {
let app = test_data::test_app(); let app = test_data::test_app();
let router = Router::new(vec![Route::accounts()]);
AccountManagementPreview { app } AccountsPreview { app, router }
} }
} }
impl View for AccountManagementPreview { impl View for AccountsPreview {
fn ui(&mut self, ui: &mut egui::Ui) { fn ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(24.0); ui.add_space(24.0);
if let Some(response) = AccountManagementView::ui( // TODO(jb55): maybe just use render_nav here so we can step through routes
ui, if let Some(response) =
&self.app.accounts, AccountsView::new(&self.app.ndb, &self.app.accounts, &mut self.app.img_cache)
&self.app.ndb, .ui(ui)
&mut self.app.img_cache, .inner
)
.inner
{ {
process_management_view_response_stateless(&mut self.app.accounts, response) process_accounts_view_response(self.app.accounts_mut(), response, &mut self.router);
} }
} }
} }
impl Preview for AccountManagementView { impl<'a> Preview for AccountsView<'a> {
type Prev = AccountManagementPreview; type Prev = AccountsPreview;
fn preview(_cfg: PreviewConfig) -> Self::Prev { fn preview(_cfg: PreviewConfig) -> Self::Prev {
AccountManagementPreview::new() AccountsPreview::new()
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
account_manager::UserAccount, colors::PINK, profile::DisplayName, ui, colors::PINK, profile::DisplayName, ui, ui::profile_preview_controller,
ui::profile_preview_controller, Damus, Result, user_account::UserAccount, Damus, Result,
}; };
use nostrdb::Ndb; use nostrdb::Ndb;
@@ -48,17 +48,19 @@ impl AccountSelectionWidget {
fn perform_action(app: &mut Damus, action: AccountSelectAction) { fn perform_action(app: &mut Damus, action: AccountSelectAction) {
match action { match action {
AccountSelectAction::RemoveAccount { _index } => app.accounts.remove_account(_index), AccountSelectAction::RemoveAccount { _index } => {
app.accounts_mut().remove_account(_index)
}
AccountSelectAction::SelectAccount { _index } => { AccountSelectAction::SelectAccount { _index } => {
app.show_account_switcher = false; app.show_account_switcher = false;
app.accounts.select_account(_index); app.accounts_mut().select_account(_index);
} }
} }
} }
fn show(app: &mut Damus, ui: &mut egui::Ui) -> (AccountSelectResponse, egui::Response) { fn show(app: &mut Damus, ui: &mut egui::Ui) -> (AccountSelectResponse, egui::Response) {
let mut res = AccountSelectResponse::default(); let mut res = AccountSelectResponse::default();
let mut selected_index = app.accounts.get_selected_account_index(); let mut selected_index = app.accounts().get_selected_account_index();
let response = Frame::none() let response = Frame::none()
.outer_margin(8.0) .outer_margin(8.0)
@@ -75,9 +77,9 @@ impl AccountSelectionWidget {
ui.add(add_account_button()); ui.add(add_account_button());
if let Some(_index) = selected_index { if let Some(_index) = selected_index {
if let Some(account) = app.accounts.get_account(_index) { if let Some(account) = app.accounts().get_account(_index) {
ui.add_space(8.0); ui.add_space(8.0);
if Self::handle_sign_out(&app.ndb, ui, account) { if Self::handle_sign_out(app.ndb(), ui, account) {
res.action = Some(AccountSelectAction::RemoveAccount { _index }) res.action = Some(AccountSelectAction::RemoveAccount { _index })
} }
} }

View File

@@ -8,12 +8,11 @@ pub mod preview;
pub mod profile; pub mod profile;
pub mod relay; pub mod relay;
pub mod side_panel; pub mod side_panel;
pub mod stateful_account_management;
pub mod thread; pub mod thread;
pub mod timeline; pub mod timeline;
pub mod username; pub mod username;
pub use account_management::AccountManagementView; pub use account_management::AccountsView;
pub use account_switcher::AccountSelectionWidget; pub use account_switcher::AccountSelectionWidget;
pub use mention::Mention; pub use mention::Mention;
pub use note::{NoteResponse, NoteView, PostReplyView, PostView}; pub use note::{NoteResponse, NoteView, PostReplyView, PostView};

View File

@@ -17,6 +17,7 @@ use crate::{
ui::View, ui::View,
}; };
use egui::{Label, RichText, Sense}; use egui::{Label, RichText, Sense};
use enostr::NoteId;
use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction}; use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction};
pub struct NoteView<'a> { pub struct NoteView<'a> {
@@ -393,7 +394,7 @@ impl<'a> NoteView<'a> {
)); ));
if self.options().has_actionbar() { if self.options().has_actionbar() {
note_action = render_note_actionbar(ui, note_key).inner; note_action = render_note_actionbar(ui, self.note.id(), note_key).inner;
} }
resp resp
@@ -430,7 +431,7 @@ impl<'a> NoteView<'a> {
)); ));
if self.options().has_actionbar() { if self.options().has_actionbar() {
note_action = render_note_actionbar(ui, note_key).inner; note_action = render_note_actionbar(ui, self.note.id(), note_key).inner;
} }
}); });
}) })
@@ -446,6 +447,7 @@ impl<'a> NoteView<'a> {
fn render_note_actionbar( fn render_note_actionbar(
ui: &mut egui::Ui, ui: &mut egui::Ui,
note_id: &[u8; 32],
note_key: NoteKey, note_key: NoteKey,
) -> egui::InnerResponse<Option<BarAction>> { ) -> egui::InnerResponse<Option<BarAction>> {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -453,9 +455,9 @@ fn render_note_actionbar(
let thread_resp = thread_button(ui, note_key); let thread_resp = thread_button(ui, note_key);
if reply_resp.clicked() { if reply_resp.clicked() {
Some(BarAction::Reply) Some(BarAction::Reply(NoteId::new(*note_id)))
} else if thread_resp.clicked() { } else if thread_resp.clicked() {
Some(BarAction::OpenThread) Some(BarAction::OpenThread(NoteId::new(*note_id)))
} else { } else {
None None
} }

View File

@@ -41,32 +41,32 @@ pub fn view_profile_previews(
) -> Option<usize> { ) -> Option<usize> {
let width = ui.available_width(); let width = ui.available_width();
let txn = if let Ok(txn) = Transaction::new(&app.ndb) { let txn = if let Ok(txn) = Transaction::new(app.ndb()) {
txn txn
} else { } else {
return None; return None;
}; };
for i in 0..app.accounts.num_accounts() { for i in 0..app.accounts().num_accounts() {
let account = if let Some(account) = app.accounts.get_account(i) { let account = if let Some(account) = app.accounts().get_account(i) {
account account
} else { } else {
continue; continue;
}; };
let profile = app let profile = app
.ndb .ndb()
.get_profile_by_pubkey(&txn, account.pubkey.bytes()) .get_profile_by_pubkey(&txn, account.pubkey.bytes())
.ok(); .ok();
let preview = SimpleProfilePreview::new(profile.as_ref(), &mut app.img_cache); let is_selected = if let Some(selected) = app.accounts().get_selected_account_index() {
let is_selected = if let Some(selected) = app.accounts.get_selected_account_index() {
i == selected i == selected
} else { } else {
false false
}; };
let preview = SimpleProfilePreview::new(profile.as_ref(), app.img_cache_mut());
if add_preview_ui(ui, preview, width, is_selected, i) { if add_preview_ui(ui, preview, width, is_selected, i) {
return Some(i); return Some(i);
} }
@@ -91,16 +91,16 @@ pub fn show_with_selected_pfp(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response, ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response,
) -> Option<egui::Response> { ) -> Option<egui::Response> {
let selected_account = app.accounts.get_selected_account(); let selected_account = app.accounts().get_selected_account();
if let Some(selected_account) = selected_account { if let Some(selected_account) = selected_account {
if let Ok(txn) = Transaction::new(&app.ndb) { if let Ok(txn) = Transaction::new(app.ndb()) {
let profile = app let profile = app
.ndb .ndb()
.get_profile_by_pubkey(&txn, selected_account.pubkey.bytes()); .get_profile_by_pubkey(&txn, selected_account.pubkey.bytes());
return Some(ui_element( return Some(ui_element(
ui, ui,
ProfilePic::new(&mut app.img_cache, get_profile_url(profile.ok().as_ref())), ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())),
)); ));
} }
} }
@@ -114,12 +114,12 @@ pub fn show_with_pfp(
key: &[u8; 32], key: &[u8; 32],
ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response, ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response,
) -> Option<egui::Response> { ) -> Option<egui::Response> {
if let Ok(txn) = Transaction::new(&app.ndb) { if let Ok(txn) = Transaction::new(app.ndb()) {
let profile = app.ndb.get_profile_by_pubkey(&txn, key); let profile = app.ndb().get_profile_by_pubkey(&txn, key);
return Some(ui_element( return Some(ui_element(
ui, ui,
ProfilePic::new(&mut app.img_cache, get_profile_url(profile.ok().as_ref())), ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())),
)); ));
} }
None None

View File

@@ -1,131 +0,0 @@
use egui::Ui;
use egui_nav::{Nav, NavAction};
use nostrdb::Ndb;
use crate::{
account_manager::{process_login_view_response, AccountManager},
imgcache::ImageCache,
login_manager::LoginState,
routable_widget_state::RoutableWidgetState,
route::{ManageAccountRoute, ManageAcountRouteResponse},
Damus,
};
use super::{
account_login_view::AccountLoginView, account_management::AccountManagementViewResponse,
AccountManagementView,
};
pub struct StatefulAccountManagementView {}
impl StatefulAccountManagementView {
pub fn show(
ui: &mut Ui,
account_management_state: &mut RoutableWidgetState<ManageAccountRoute>,
account_manager: &mut AccountManager,
img_cache: &mut ImageCache,
login_state: &mut LoginState,
ndb: &Ndb,
) {
let routes = account_management_state.get_routes();
let nav_response =
Nav::new(routes)
.title(false)
.navigating(false)
.show_mut(ui, |ui, nav| match nav.top() {
ManageAccountRoute::AccountManagement => {
AccountManagementView::ui(ui, account_manager, ndb, img_cache)
.inner
.map(ManageAcountRouteResponse::AccountManagement)
}
ManageAccountRoute::AddAccount => AccountLoginView::new(login_state)
.ui(ui)
.inner
.map(ManageAcountRouteResponse::AddAccount),
});
if let Some(resp) = nav_response.inner {
match resp {
ManageAcountRouteResponse::AccountManagement(response) => {
process_management_view_response_stateful(
response,
account_manager,
account_management_state,
);
}
ManageAcountRouteResponse::AddAccount(response) => {
process_login_view_response(account_manager, response);
*login_state = Default::default();
account_management_state.go_back();
}
}
}
if let Some(NavAction::Returned) = nav_response.action {
account_management_state.go_back();
}
}
}
pub fn process_management_view_response_stateful(
response: AccountManagementViewResponse,
manager: &mut AccountManager,
state: &mut RoutableWidgetState<ManageAccountRoute>,
) {
match response {
AccountManagementViewResponse::RemoveAccount(index) => {
manager.remove_account(index);
}
AccountManagementViewResponse::SelectAccount(index) => {
manager.select_account(index);
}
AccountManagementViewResponse::RouteToLogin => {
state.route_to(ManageAccountRoute::AddAccount);
}
}
}
mod preview {
use crate::{
test_data,
ui::{Preview, PreviewConfig, View},
};
use super::*;
pub struct StatefulAccountManagementPreview {
app: Damus,
}
impl StatefulAccountManagementPreview {
fn new() -> Self {
let mut app = test_data::test_app();
app.account_management_view_state
.route_to(ManageAccountRoute::AccountManagement);
StatefulAccountManagementPreview { app }
}
}
impl View for StatefulAccountManagementPreview {
fn ui(&mut self, ui: &mut egui::Ui) {
StatefulAccountManagementView::show(
ui,
&mut self.app.account_management_view_state,
&mut self.app.accounts,
&mut self.app.img_cache,
&mut self.app.login_state,
&self.app.ndb,
);
}
}
impl Preview for StatefulAccountManagementView {
type Prev = StatefulAccountManagementPreview;
fn preview(cfg: PreviewConfig) -> Self::Prev {
let _ = cfg;
StatefulAccountManagementPreview::new()
}
}
}

View File

@@ -1,55 +1,49 @@
use crate::{ use crate::{
actionbar::BarResult, column::Columns, imgcache::ImageCache, notecache::NoteCache, actionbar::BarAction, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui,
thread::Threads, timeline::TimelineSource, ui, unknowns::UnknownIds,
}; };
use enostr::RelayPool;
use nostrdb::{Ndb, NoteKey, Transaction}; use nostrdb::{Ndb, NoteKey, Transaction};
use tracing::{error, warn}; use tracing::{error, warn};
pub struct ThreadView<'a> { pub struct ThreadView<'a> {
column: usize,
columns: &'a mut Columns,
threads: &'a mut Threads, threads: &'a mut Threads,
ndb: &'a Ndb, ndb: &'a Ndb,
pool: &'a mut RelayPool,
note_cache: &'a mut NoteCache, note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache, img_cache: &'a mut ImageCache,
unknown_ids: &'a mut UnknownIds,
selected_note_id: &'a [u8; 32], selected_note_id: &'a [u8; 32],
textmode: bool, textmode: bool,
id_source: egui::Id,
} }
impl<'a> ThreadView<'a> { impl<'a> ThreadView<'a> {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
column: usize,
columns: &'a mut Columns,
threads: &'a mut Threads, threads: &'a mut Threads,
ndb: &'a Ndb, ndb: &'a Ndb,
note_cache: &'a mut NoteCache, note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache, img_cache: &'a mut ImageCache,
unknown_ids: &'a mut UnknownIds,
pool: &'a mut RelayPool,
textmode: bool,
selected_note_id: &'a [u8; 32], selected_note_id: &'a [u8; 32],
textmode: bool,
) -> Self { ) -> Self {
let id_source = egui::Id::new("threadscroll_threadview");
ThreadView { ThreadView {
column,
columns,
threads, threads,
ndb, ndb,
note_cache, note_cache,
img_cache, img_cache,
textmode,
selected_note_id, selected_note_id,
unknown_ids, textmode,
pool, id_source,
} }
} }
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<BarResult> { pub fn id_source(mut self, id: egui::Id) -> Self {
self.id_source = id;
self
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<BarAction> {
let txn = Transaction::new(self.ndb).expect("txn"); let txn = Transaction::new(self.ndb).expect("txn");
let mut result: Option<BarResult> = None; let mut action: Option<BarAction> = None;
let selected_note_key = if let Ok(key) = self let selected_note_key = if let Ok(key) = self
.ndb .ndb
@@ -62,21 +56,13 @@ impl<'a> ThreadView<'a> {
return None; return None;
}; };
let scroll_id = {
egui::Id::new((
"threadscroll",
self.columns.column(self.column).view_id(),
selected_note_key,
))
};
ui.label( ui.label(
egui::RichText::new("Threads ALPHA! It's not done. Things will be broken.") egui::RichText::new("Threads ALPHA! It's not done. Things will be broken.")
.color(egui::Color32::RED), .color(egui::Color32::RED),
); );
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.id_source(scroll_id) .id_source(self.id_source)
.animated(false) .animated(false)
.auto_shrink([false, false]) .auto_shrink([false, false])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
@@ -99,36 +85,27 @@ impl<'a> ThreadView<'a> {
.map_or_else(|| self.selected_note_id, |nr| nr.id) .map_or_else(|| self.selected_note_id, |nr| nr.id)
}; };
let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr();
// TODO(jb55): skip poll if ThreadResult is fresh?
// poll for new notes and insert them into our existing notes // poll for new notes and insert them into our existing notes
if let Err(e) = TimelineSource::Thread(root_id).poll_notes_into_view( if let Err(e) = thread.poll_notes_into_view(&txn, self.ndb) {
&txn,
self.ndb,
self.columns,
self.threads,
self.unknown_ids,
self.note_cache,
) {
error!("Thread::poll_notes_into_view: {e}"); error!("Thread::poll_notes_into_view: {e}");
} }
let (len, list) = { let len = thread.view().notes.len();
let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr();
let len = thread.view.notes.len(); thread.view().list.clone().borrow_mut().ui_custom_layout(
(len, &mut thread.view.list) ui,
}; len,
|ui, start_index| {
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.y = 0.0;
ui.spacing_mut().item_spacing.x = 4.0; ui.spacing_mut().item_spacing.x = 4.0;
let ind = len - 1 - start_index; let ind = len - 1 - start_index;
let note_key = {
let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr(); let note_key = thread.view().notes[ind].key;
thread.view.notes[ind].key
};
let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, note_key) { let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, note_key) {
note note
@@ -138,25 +115,14 @@ impl<'a> ThreadView<'a> {
}; };
ui::padding(8.0, ui, |ui| { ui::padding(8.0, ui, |ui| {
let resp = if let Some(bar_action) =
ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, &note) ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, &note)
.note_previews(!self.textmode) .note_previews(!self.textmode)
.textmode(self.textmode) .textmode(self.textmode)
.show(ui); .show(ui)
.action
if let Some(action) = resp.action { {
let br = action.execute( action = Some(bar_action);
self.ndb,
self.columns.column_mut(self.column),
self.threads,
self.note_cache,
self.pool,
note.id(),
&txn,
);
if br.is_some() {
result = br;
}
} }
}); });
@@ -164,9 +130,10 @@ impl<'a> ThreadView<'a> {
//ui.add(egui::Separator::default().spacing(0.0)); //ui.add(egui::Separator::default().spacing(0.0));
1 1
}); },
);
}); });
result action
} }
} }

View File

@@ -1,67 +1,56 @@
use crate::{ use crate::{
actionbar::BarAction, actionbar::BarAction, column::Columns, draft::Drafts, imgcache::ImageCache,
actionbar::BarResult, notecache::NoteCache, timeline::TimelineId, ui, ui::note::PostAction,
column::{Column, ColumnKind},
draft::Drafts,
imgcache::ImageCache,
notecache::NoteCache,
thread::Threads,
ui,
ui::note::PostAction,
}; };
use egui::containers::scroll_area::ScrollBarVisibility; use egui::containers::scroll_area::ScrollBarVisibility;
use egui::{Direction, Layout}; use egui::{Direction, Layout};
use egui_tabs::TabColor; use egui_tabs::TabColor;
use enostr::{FilledKeypair, RelayPool}; use enostr::{FilledKeypair, RelayPool};
use nostrdb::{Ndb, Note, Transaction}; use nostrdb::{Ndb, Transaction};
use tracing::{debug, info, warn}; use tracing::{debug, error, info, warn};
pub struct TimelineView<'a> { pub struct TimelineView<'a> {
timeline_id: TimelineId,
columns: &'a mut Columns,
ndb: &'a Ndb, ndb: &'a Ndb,
column: &'a mut Column,
note_cache: &'a mut NoteCache, note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache, img_cache: &'a mut ImageCache,
threads: &'a mut Threads,
pool: &'a mut RelayPool,
textmode: bool, textmode: bool,
reverse: bool, reverse: bool,
} }
impl<'a> TimelineView<'a> { impl<'a> TimelineView<'a> {
pub fn new( pub fn new(
timeline_id: TimelineId,
columns: &'a mut Columns,
ndb: &'a Ndb, ndb: &'a Ndb,
column: &'a mut Column,
note_cache: &'a mut NoteCache, note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache, img_cache: &'a mut ImageCache,
threads: &'a mut Threads,
pool: &'a mut RelayPool,
textmode: bool, textmode: bool,
) -> TimelineView<'a> { ) -> TimelineView<'a> {
let reverse = false; let reverse = false;
TimelineView { TimelineView {
ndb, ndb,
column, timeline_id,
columns,
note_cache, note_cache,
img_cache, img_cache,
threads,
pool,
reverse, reverse,
textmode, textmode,
} }
} }
pub fn ui(&mut self, ui: &mut egui::Ui) { pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<BarAction> {
timeline_ui( timeline_ui(
ui, ui,
self.ndb, self.ndb,
self.column, self.timeline_id,
self.columns,
self.note_cache, self.note_cache,
self.img_cache, self.img_cache,
self.threads,
self.pool,
self.reverse, self.reverse,
self.textmode, self.textmode,
); )
} }
pub fn reversed(mut self) -> Self { pub fn reversed(mut self) -> Self {
@@ -74,14 +63,13 @@ impl<'a> TimelineView<'a> {
fn timeline_ui( fn timeline_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ndb: &Ndb, ndb: &Ndb,
column: &mut Column, timeline_id: TimelineId,
columns: &mut Columns,
note_cache: &mut NoteCache, note_cache: &mut NoteCache,
img_cache: &mut ImageCache, img_cache: &mut ImageCache,
threads: &mut Threads,
pool: &mut RelayPool,
reversed: bool, reversed: bool,
textmode: bool, textmode: bool,
) { ) -> Option<BarAction> {
//padding(4.0, ui, |ui| ui.heading("Notifications")); //padding(4.0, ui, |ui| ui.heading("Notifications"));
/* /*
let font_id = egui::TextStyle::Body.resolve(ui.style()); let font_id = egui::TextStyle::Body.resolve(ui.style());
@@ -89,29 +77,37 @@ fn timeline_ui(
*/ */
{ let scroll_id = {
let timeline = if let ColumnKind::Timeline(timeline) = column.kind_mut() { let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) {
timeline timeline
} else { } else {
return; error!("tried to render timeline in column, but timeline was missing");
// TODO (jb55): render error when timeline is missing?
// this shouldn't happen...
return None;
}; };
timeline.selected_view = tabs_ui(ui); timeline.selected_view = tabs_ui(ui);
// need this for some reason?? // need this for some reason??
ui.add_space(3.0); ui.add_space(3.0);
}
let scroll_id = egui::Id::new(("tlscroll", column.view_id())); egui::Id::new(("tlscroll", timeline.view_id()))
};
let mut bar_action: Option<BarAction> = None;
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.id_source(scroll_id) .id_source(scroll_id)
.animated(false) .animated(false)
.auto_shrink([false, false]) .auto_shrink([false, false])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
.show(ui, |ui| { .show(ui, |ui| {
let timeline = if let ColumnKind::Timeline(timeline) = column.kind_mut() { let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) {
timeline timeline
} else { } else {
error!("tried to render timeline in column, but timeline was missing");
// TODO (jb55): render error when timeline is missing?
// this shouldn't happen...
return 0; return 0;
}; };
@@ -124,7 +120,6 @@ fn timeline_ui(
return 0; return 0;
}; };
let mut bar_action: Option<(BarAction, Note)> = None;
view.list view.list
.clone() .clone()
.borrow_mut() .borrow_mut()
@@ -154,7 +149,7 @@ fn timeline_ui(
.show(ui); .show(ui);
if let Some(ba) = resp.action { if let Some(ba) = resp.action {
bar_action = Some((ba, note)); bar_action = Some(ba);
} else if resp.response.clicked() { } else if resp.response.clicked() {
debug!("clicked note"); debug!("clicked note");
} }
@@ -166,25 +161,10 @@ fn timeline_ui(
1 1
}); });
// handle any actions from the virtual list
if let Some((action, note)) = bar_action {
if let Some(br) =
action.execute(ndb, column, threads, note_cache, pool, note.id(), &txn)
{
match br {
// update the thread for next render if we have new notes
BarResult::NewThreadNotes(new_notes) => {
let thread = threads
.thread_mut(ndb, &txn, new_notes.root_id.bytes())
.get_ptr();
new_notes.process(thread);
}
}
}
}
1 1
}); });
bar_action
} }
pub fn postbox_view<'a>( pub fn postbox_view<'a>(

View File

@@ -1,11 +1,10 @@
use notedeck::app_creation::{ use notedeck::app_creation::{
generate_mobile_emulator_native_options, generate_native_options, setup_cc, generate_mobile_emulator_native_options, generate_native_options, setup_cc,
}; };
use notedeck::ui::account_login_view::AccountLoginView;
use notedeck::ui::stateful_account_management::StatefulAccountManagementView;
use notedeck::ui::{ use notedeck::ui::{
AccountManagementView, AccountSelectionWidget, DesktopSidePanel, PostView, Preview, PreviewApp, account_login_view::AccountLoginView, account_management::AccountsView, AccountSelectionWidget,
PreviewConfig, ProfilePic, ProfilePreview, RelayView, DesktopSidePanel, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview,
RelayView,
}; };
use std::env; use std::env;
@@ -101,10 +100,9 @@ async fn main() {
AccountLoginView, AccountLoginView,
ProfilePreview, ProfilePreview,
ProfilePic, ProfilePic,
AccountManagementView, AccountsView,
AccountSelectionWidget, AccountSelectionWidget,
DesktopSidePanel, DesktopSidePanel,
PostView, PostView,
StatefulAccountManagementView,
); );
} }

View File

@@ -1,7 +1,11 @@
use crate::column::Columns; use crate::{
use crate::notecache::{CachedNote, NoteCache}; column::Columns,
use crate::timeline::ViewFilter; note::NoteRef,
use crate::Result; notecache::{CachedNote, NoteCache},
timeline::ViewFilter,
Result,
};
use enostr::{Filter, NoteId, Pubkey}; use enostr::{Filter, NoteId, Pubkey};
use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
use std::collections::HashSet; use std::collections::HashSet;
@@ -64,6 +68,35 @@ impl UnknownIds {
self.last_updated = Some(now); self.last_updated = Some(now);
} }
pub fn update_from_note_key(
txn: &Transaction,
ndb: &Ndb,
unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache,
key: NoteKey,
) -> bool {
let note = if let Ok(note) = ndb.get_note_by_key(txn, key) {
note
} else {
return false;
};
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note)
}
/// Should be called on freshly polled notes from subscriptions
pub fn update_from_note_refs(
txn: &Transaction,
ndb: &Ndb,
unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache,
note_refs: &[NoteRef],
) {
for note_ref in note_refs {
Self::update_from_note_key(txn, ndb, unknown_ids, note_cache, note_ref.key);
}
}
pub fn update_from_note( pub fn update_from_note(
txn: &Transaction, txn: &Transaction,
ndb: &Ndb, ndb: &Ndb,

13
src/view_state.rs Normal file
View File

@@ -0,0 +1,13 @@
use crate::login_manager::LoginState;
/// Various state for views
#[derive(Default)]
pub struct ViewState {
pub login: LoginState,
}
impl ViewState {
pub fn login_mut(&mut self) -> &mut LoginState {
&mut self.login
}
}