notedeck

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

nav.rs (19564B)


      1 use crate::{
      2     accounts::render_accounts_route,
      3     actionbar::NoteAction,
      4     app::{get_active_columns_mut, get_decks_mut},
      5     column::ColumnsAction,
      6     deck_state::DeckState,
      7     decks::{Deck, DecksAction, DecksCache},
      8     profile::{ProfileAction, SaveProfileChanges},
      9     profile_state::ProfileState,
     10     relay_pool_manager::RelayPoolManager,
     11     route::Route,
     12     timeline::{route::render_timeline_route, TimelineCache},
     13     ui::{
     14         self,
     15         add_column::render_add_column_routes,
     16         column::NavTitle,
     17         configure_deck::ConfigureDeckView,
     18         edit_deck::{EditDeckResponse, EditDeckView},
     19         note::{contents::NoteContext, PostAction, PostType},
     20         profile::EditProfileView,
     21         search::{FocusState, SearchView},
     22         support::SupportView,
     23         RelayView, View,
     24     },
     25     Damus,
     26 };
     27 
     28 use egui_nav::{Nav, NavAction, NavResponse, NavUiType};
     29 use nostrdb::Transaction;
     30 use notedeck::{AccountsAction, AppContext};
     31 use tracing::error;
     32 
     33 #[allow(clippy::enum_variant_names)]
     34 pub enum RenderNavAction {
     35     Back,
     36     RemoveColumn,
     37     PostAction(PostAction),
     38     NoteAction(NoteAction),
     39     ProfileAction(ProfileAction),
     40     SwitchingAction(SwitchingAction),
     41 }
     42 
     43 pub enum SwitchingAction {
     44     Accounts(AccountsAction),
     45     Columns(ColumnsAction),
     46     Decks(crate::decks::DecksAction),
     47 }
     48 
     49 impl SwitchingAction {
     50     /// process the action, and return whether switching occured
     51     pub fn process(
     52         &self,
     53         timeline_cache: &mut TimelineCache,
     54         decks_cache: &mut DecksCache,
     55         ctx: &mut AppContext<'_>,
     56     ) -> bool {
     57         match &self {
     58             SwitchingAction::Accounts(account_action) => match account_action {
     59                 AccountsAction::Switch(switch_action) => {
     60                     ctx.accounts.select_account(switch_action.switch_to);
     61                     // pop nav after switch
     62                     if let Some(src) = switch_action.source {
     63                         get_active_columns_mut(ctx.accounts, decks_cache)
     64                             .column_mut(src)
     65                             .router_mut()
     66                             .go_back();
     67                     }
     68                 }
     69                 AccountsAction::Remove(index) => ctx.accounts.remove_account(*index),
     70             },
     71             SwitchingAction::Columns(columns_action) => match *columns_action {
     72                 ColumnsAction::Remove(index) => {
     73                     let kinds_to_pop =
     74                         get_active_columns_mut(ctx.accounts, decks_cache).delete_column(index);
     75                     for kind in &kinds_to_pop {
     76                         if let Err(err) = timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
     77                             error!("error popping timeline: {err}");
     78                         }
     79                     }
     80                 }
     81 
     82                 ColumnsAction::Switch(from, to) => {
     83                     get_active_columns_mut(ctx.accounts, decks_cache).move_col(from, to);
     84                 }
     85             },
     86             SwitchingAction::Decks(decks_action) => match *decks_action {
     87                 DecksAction::Switch(index) => {
     88                     get_decks_mut(ctx.accounts, decks_cache).set_active(index)
     89                 }
     90                 DecksAction::Removing(index) => {
     91                     get_decks_mut(ctx.accounts, decks_cache).remove_deck(index)
     92                 }
     93             },
     94         }
     95         true
     96     }
     97 }
     98 
     99 impl From<PostAction> for RenderNavAction {
    100     fn from(post_action: PostAction) -> Self {
    101         Self::PostAction(post_action)
    102     }
    103 }
    104 
    105 impl From<NoteAction> for RenderNavAction {
    106     fn from(note_action: NoteAction) -> RenderNavAction {
    107         Self::NoteAction(note_action)
    108     }
    109 }
    110 
    111 pub type NotedeckNavResponse = NavResponse<Option<RenderNavAction>>;
    112 
    113 pub struct RenderNavResponse {
    114     column: usize,
    115     response: NotedeckNavResponse,
    116 }
    117 
    118 impl RenderNavResponse {
    119     #[allow(private_interfaces)]
    120     pub fn new(column: usize, response: NotedeckNavResponse) -> Self {
    121         RenderNavResponse { column, response }
    122     }
    123 
    124     #[must_use = "Make sure to save columns if result is true"]
    125     pub fn process_render_nav_response(&self, app: &mut Damus, ctx: &mut AppContext<'_>) -> bool {
    126         let mut switching_occured: bool = false;
    127         let col = self.column;
    128 
    129         if let Some(action) = self
    130             .response
    131             .response
    132             .as_ref()
    133             .or(self.response.title_response.as_ref())
    134         {
    135             // start returning when we're finished posting
    136             match action {
    137                 RenderNavAction::Back => {
    138                     app.columns_mut(ctx.accounts)
    139                         .column_mut(col)
    140                         .router_mut()
    141                         .go_back();
    142                 }
    143 
    144                 RenderNavAction::RemoveColumn => {
    145                     let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col);
    146 
    147                     for kind in &kinds_to_pop {
    148                         if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
    149                             error!("error popping timeline: {err}");
    150                         }
    151                     }
    152 
    153                     switching_occured = true;
    154                 }
    155 
    156                 RenderNavAction::PostAction(post_action) => {
    157                     let txn = Transaction::new(ctx.ndb).expect("txn");
    158                     let _ = post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts);
    159                     get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    160                         .column_mut(col)
    161                         .router_mut()
    162                         .go_back();
    163                 }
    164 
    165                 RenderNavAction::NoteAction(note_action) => {
    166                     let txn = Transaction::new(ctx.ndb).expect("txn");
    167 
    168                     note_action.execute_and_process_result(
    169                         ctx.ndb,
    170                         get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
    171                         col,
    172                         &mut app.timeline_cache,
    173                         ctx.note_cache,
    174                         ctx.pool,
    175                         &txn,
    176                         ctx.unknown_ids,
    177                     );
    178                 }
    179 
    180                 RenderNavAction::SwitchingAction(switching_action) => {
    181                     switching_occured = switching_action.process(
    182                         &mut app.timeline_cache,
    183                         &mut app.decks_cache,
    184                         ctx,
    185                     );
    186                 }
    187                 RenderNavAction::ProfileAction(profile_action) => {
    188                     profile_action.process(
    189                         &mut app.view_state.pubkey_to_profile_state,
    190                         ctx.ndb,
    191                         ctx.pool,
    192                         get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    193                             .column_mut(col)
    194                             .router_mut(),
    195                     );
    196                 }
    197             }
    198         }
    199 
    200         if let Some(action) = self.response.action {
    201             match action {
    202                 NavAction::Returned => {
    203                     let r = app
    204                         .columns_mut(ctx.accounts)
    205                         .column_mut(col)
    206                         .router_mut()
    207                         .pop();
    208 
    209                     if let Some(Route::Timeline(kind)) = &r {
    210                         if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
    211                             error!("popping timeline had an error: {err} for {:?}", kind);
    212                         }
    213                     };
    214 
    215                     switching_occured = true;
    216                 }
    217 
    218                 NavAction::Navigated => {
    219                     let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut();
    220                     cur_router.navigating = false;
    221                     if cur_router.is_replacing() {
    222                         cur_router.remove_previous_routes();
    223                     }
    224                     switching_occured = true;
    225                 }
    226 
    227                 NavAction::Dragging => {}
    228                 NavAction::Returning => {}
    229                 NavAction::Resetting => {}
    230                 NavAction::Navigating => {}
    231             }
    232         }
    233 
    234         switching_occured
    235     }
    236 }
    237 
    238 fn render_nav_body(
    239     ui: &mut egui::Ui,
    240     app: &mut Damus,
    241     ctx: &mut AppContext<'_>,
    242     top: &Route,
    243     depth: usize,
    244     col: usize,
    245     inner_rect: egui::Rect,
    246 ) -> Option<RenderNavAction> {
    247     let mut note_context = NoteContext {
    248         ndb: ctx.ndb,
    249         img_cache: ctx.img_cache,
    250         note_cache: ctx.note_cache,
    251     };
    252     match top {
    253         Route::Timeline(kind) => render_timeline_route(
    254             ctx.unknown_ids,
    255             &mut app.timeline_cache,
    256             ctx.accounts,
    257             kind,
    258             col,
    259             app.note_options,
    260             depth,
    261             ui,
    262             &mut note_context,
    263         ),
    264 
    265         Route::Accounts(amr) => {
    266             let mut action = render_accounts_route(
    267                 ui,
    268                 ctx.ndb,
    269                 col,
    270                 ctx.img_cache,
    271                 ctx.accounts,
    272                 &mut app.decks_cache,
    273                 &mut app.view_state.login,
    274                 *amr,
    275             );
    276             let txn = Transaction::new(ctx.ndb).expect("txn");
    277             action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
    278             action
    279                 .accounts_action
    280                 .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
    281         }
    282 
    283         Route::Relays => {
    284             let manager = RelayPoolManager::new(ctx.pool);
    285             RelayView::new(ctx.accounts, manager, &mut app.view_state.id_string_map).ui(ui);
    286             None
    287         }
    288 
    289         Route::Reply(id) => {
    290             let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
    291                 txn
    292             } else {
    293                 ui.label("Reply to unknown note");
    294                 return None;
    295             };
    296 
    297             let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
    298                 note
    299             } else {
    300                 ui.label("Reply to unknown note");
    301                 return None;
    302             };
    303 
    304             let id = egui::Id::new(("post", col, note.key().unwrap()));
    305             let poster = ctx.accounts.selected_or_first_nsec()?;
    306 
    307             let action = {
    308                 let draft = app.drafts.reply_mut(note.id());
    309 
    310                 let response = egui::ScrollArea::vertical()
    311                     .show(ui, |ui| {
    312                         ui::PostReplyView::new(
    313                             &mut note_context,
    314                             poster,
    315                             draft,
    316                             &note,
    317                             inner_rect,
    318                             app.note_options,
    319                         )
    320                         .id_source(id)
    321                         .show(ui)
    322                     })
    323                     .inner;
    324 
    325                 if let Some(selection) = response.context_selection {
    326                     selection.process(ui, &note);
    327                 }
    328 
    329                 response.action
    330             };
    331 
    332             action.map(Into::into)
    333         }
    334 
    335         Route::Quote(id) => {
    336             let txn = Transaction::new(ctx.ndb).expect("txn");
    337 
    338             let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
    339                 note
    340             } else {
    341                 ui.label("Quote of unknown note");
    342                 return None;
    343             };
    344 
    345             let id = egui::Id::new(("post", col, note.key().unwrap()));
    346 
    347             let poster = ctx.accounts.selected_or_first_nsec()?;
    348             let draft = app.drafts.quote_mut(note.id());
    349 
    350             let response = egui::ScrollArea::vertical()
    351                 .show(ui, |ui| {
    352                     crate::ui::note::QuoteRepostView::new(
    353                         &mut note_context,
    354                         poster,
    355                         draft,
    356                         &note,
    357                         inner_rect,
    358                         app.note_options,
    359                     )
    360                     .id_source(id)
    361                     .show(ui)
    362                 })
    363                 .inner;
    364 
    365             if let Some(selection) = response.context_selection {
    366                 selection.process(ui, &note);
    367             }
    368 
    369             response.action.map(Into::into)
    370         }
    371 
    372         Route::ComposeNote => {
    373             let kp = ctx.accounts.get_selected_account()?.to_full()?;
    374             let draft = app.drafts.compose_mut();
    375 
    376             let txn = Transaction::new(ctx.ndb).expect("txn");
    377             let post_response = ui::PostView::new(
    378                 &mut note_context,
    379                 draft,
    380                 PostType::New,
    381                 kp,
    382                 inner_rect,
    383                 app.note_options,
    384             )
    385             .ui(&txn, ui);
    386 
    387             post_response.action.map(Into::into)
    388         }
    389 
    390         Route::AddColumn(route) => {
    391             render_add_column_routes(ui, app, ctx, col, route);
    392 
    393             None
    394         }
    395 
    396         Route::Support => {
    397             SupportView::new(&mut app.support).show(ui);
    398             None
    399         }
    400 
    401         Route::Search => {
    402             let id = ui.id().with(("search", depth, col));
    403             let navigating = app
    404                 .columns_mut(ctx.accounts)
    405                 .column(col)
    406                 .router()
    407                 .navigating;
    408             let search_buffer = app.view_state.searches.entry(id).or_default();
    409             let txn = Transaction::new(ctx.ndb).expect("txn");
    410 
    411             if navigating {
    412                 search_buffer.focus_state = FocusState::Navigating
    413             } else if search_buffer.focus_state == FocusState::Navigating {
    414                 // we're not navigating but our last search buffer state
    415                 // says we were navigating. This means that navigating has
    416                 // stopped. Let's make sure to focus the input field
    417                 search_buffer.focus_state = FocusState::ShouldRequestFocus;
    418             }
    419 
    420             SearchView::new(
    421                 &txn,
    422                 &ctx.accounts.mutefun(),
    423                 app.note_options,
    424                 search_buffer,
    425                 &mut note_context,
    426             )
    427             .show(ui)
    428             .map(RenderNavAction::NoteAction)
    429         }
    430 
    431         Route::NewDeck => {
    432             let id = ui.id().with("new-deck");
    433             let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
    434             let mut resp = None;
    435             if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) {
    436                 if let Some(cur_acc) = ctx.accounts.get_selected_account() {
    437                     app.decks_cache.add_deck(
    438                         cur_acc.pubkey,
    439                         Deck::new(config_resp.icon, config_resp.name),
    440                     );
    441 
    442                     // set new deck as active
    443                     let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache)
    444                         .decks()
    445                         .len()
    446                         - 1;
    447                     resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
    448                         DecksAction::Switch(cur_index),
    449                     )));
    450                 }
    451 
    452                 new_deck_state.clear();
    453                 get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    454                     .get_first_router()
    455                     .go_back();
    456             }
    457             resp
    458         }
    459         Route::EditDeck(index) => {
    460             let mut action = None;
    461             let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache)
    462                 .decks_mut()
    463                 .get_mut(*index)
    464                 .expect("index wasn't valid");
    465             let id = ui.id().with((
    466                 "edit-deck",
    467                 ctx.accounts.get_selected_account().map(|k| k.pubkey),
    468                 index,
    469             ));
    470             let deck_state = app
    471                 .view_state
    472                 .id_to_deck_state
    473                 .entry(id)
    474                 .or_insert_with(|| DeckState::from_deck(cur_deck));
    475             if let Some(resp) = EditDeckView::new(deck_state).ui(ui) {
    476                 match resp {
    477                     EditDeckResponse::Edit(configure_deck_response) => {
    478                         cur_deck.edit(configure_deck_response);
    479                     }
    480                     EditDeckResponse::Delete => {
    481                         action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
    482                             DecksAction::Removing(*index),
    483                         )));
    484                     }
    485                 }
    486                 get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    487                     .get_first_router()
    488                     .go_back();
    489             }
    490 
    491             action
    492         }
    493         Route::EditProfile(pubkey) => {
    494             let mut action = None;
    495             if let Some(kp) = ctx.accounts.get_full(pubkey.bytes()) {
    496                 let state = app
    497                     .view_state
    498                     .pubkey_to_profile_state
    499                     .entry(*kp.pubkey)
    500                     .or_insert_with(|| {
    501                         let txn = Transaction::new(ctx.ndb).expect("txn");
    502                         if let Ok(record) = ctx.ndb.get_profile_by_pubkey(&txn, kp.pubkey.bytes()) {
    503                             ProfileState::from_profile(&record)
    504                         } else {
    505                             ProfileState::default()
    506                         }
    507                     });
    508                 if EditProfileView::new(state, ctx.img_cache).ui(ui) {
    509                     if let Some(taken_state) =
    510                         app.view_state.pubkey_to_profile_state.remove(kp.pubkey)
    511                     {
    512                         action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
    513                             SaveProfileChanges::new(kp.to_full(), taken_state),
    514                         )))
    515                     }
    516                 }
    517             } else {
    518                 error!("Pubkey in EditProfile route did not have an nsec attached in Accounts");
    519             }
    520             action
    521         }
    522     }
    523 }
    524 
    525 #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
    526 pub fn render_nav(
    527     col: usize,
    528     inner_rect: egui::Rect,
    529     app: &mut Damus,
    530     ctx: &mut AppContext<'_>,
    531     ui: &mut egui::Ui,
    532 ) -> RenderNavResponse {
    533     let nav_response = Nav::new(
    534         &app.columns(ctx.accounts)
    535             .column(col)
    536             .router()
    537             .routes()
    538             .clone(),
    539     )
    540     .navigating(
    541         app.columns_mut(ctx.accounts)
    542             .column_mut(col)
    543             .router_mut()
    544             .navigating,
    545     )
    546     .returning(
    547         app.columns_mut(ctx.accounts)
    548             .column_mut(col)
    549             .router_mut()
    550             .returning,
    551     )
    552     .id_source(egui::Id::new(("nav", col)))
    553     .show_mut(ui, |ui, render_type, nav| match render_type {
    554         NavUiType::Title => NavTitle::new(
    555             ctx.ndb,
    556             ctx.img_cache,
    557             get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
    558             nav.routes(),
    559             col,
    560         )
    561         .show(ui),
    562         NavUiType::Body => {
    563             if let Some(top) = nav.routes().last() {
    564                 render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect)
    565             } else {
    566                 None
    567             }
    568         }
    569     });
    570 
    571     RenderNavResponse::new(col, nav_response)
    572 }