notedeck

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

nav.rs (17144B)


      1 use crate::{
      2     accounts::render_accounts_route,
      3     actionbar::NoteAction,
      4     app::{get_active_columns, 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::{
     13         route::{render_timeline_route, TimelineRoute},
     14         Timeline,
     15     },
     16     ui::{
     17         self,
     18         add_column::render_add_column_routes,
     19         column::NavTitle,
     20         configure_deck::ConfigureDeckView,
     21         edit_deck::{EditDeckResponse, EditDeckView},
     22         note::{PostAction, PostType},
     23         profile::EditProfileView,
     24         support::SupportView,
     25         RelayView, View,
     26     },
     27     Damus,
     28 };
     29 
     30 use notedeck::{AccountsAction, AppContext, RootIdError};
     31 
     32 use egui_nav::{Nav, NavAction, NavResponse, NavUiType};
     33 use nostrdb::{Ndb, Transaction};
     34 use tracing::{error, info};
     35 
     36 #[allow(clippy::enum_variant_names)]
     37 pub enum RenderNavAction {
     38     Back,
     39     RemoveColumn,
     40     PostAction(PostAction),
     41     NoteAction(NoteAction),
     42     ProfileAction(ProfileAction),
     43     SwitchingAction(SwitchingAction),
     44 }
     45 
     46 pub enum SwitchingAction {
     47     Accounts(AccountsAction),
     48     Columns(ColumnsAction),
     49     Decks(crate::decks::DecksAction),
     50 }
     51 
     52 impl SwitchingAction {
     53     /// process the action, and return whether switching occured
     54     pub fn process(&self, decks_cache: &mut DecksCache, ctx: &mut AppContext<'_>) -> bool {
     55         match &self {
     56             SwitchingAction::Accounts(account_action) => match account_action {
     57                 AccountsAction::Switch(switch_action) => {
     58                     ctx.accounts.select_account(switch_action.switch_to);
     59                     // pop nav after switch
     60                     if let Some(src) = switch_action.source {
     61                         get_active_columns_mut(ctx.accounts, decks_cache)
     62                             .column_mut(src)
     63                             .router_mut()
     64                             .go_back();
     65                     }
     66                 }
     67                 AccountsAction::Remove(index) => ctx.accounts.remove_account(*index),
     68             },
     69             SwitchingAction::Columns(columns_action) => match *columns_action {
     70                 ColumnsAction::Remove(index) => {
     71                     get_active_columns_mut(ctx.accounts, decks_cache).delete_column(index)
     72                 }
     73                 ColumnsAction::Switch(from, to) => {
     74                     get_active_columns_mut(ctx.accounts, decks_cache).move_col(from, to);
     75                 }
     76             },
     77             SwitchingAction::Decks(decks_action) => match *decks_action {
     78                 DecksAction::Switch(index) => {
     79                     get_decks_mut(ctx.accounts, decks_cache).set_active(index)
     80                 }
     81                 DecksAction::Removing(index) => {
     82                     get_decks_mut(ctx.accounts, decks_cache).remove_deck(index)
     83                 }
     84             },
     85         }
     86         true
     87     }
     88 }
     89 
     90 impl From<PostAction> for RenderNavAction {
     91     fn from(post_action: PostAction) -> Self {
     92         Self::PostAction(post_action)
     93     }
     94 }
     95 
     96 impl From<NoteAction> for RenderNavAction {
     97     fn from(note_action: NoteAction) -> RenderNavAction {
     98         Self::NoteAction(note_action)
     99     }
    100 }
    101 
    102 pub type NotedeckNavResponse = NavResponse<Option<RenderNavAction>>;
    103 
    104 pub struct RenderNavResponse {
    105     column: usize,
    106     response: NotedeckNavResponse,
    107 }
    108 
    109 impl RenderNavResponse {
    110     #[allow(private_interfaces)]
    111     pub fn new(column: usize, response: NotedeckNavResponse) -> Self {
    112         RenderNavResponse { column, response }
    113     }
    114 
    115     #[must_use = "Make sure to save columns if result is true"]
    116     pub fn process_render_nav_response(&self, app: &mut Damus, ctx: &mut AppContext<'_>) -> bool {
    117         let mut switching_occured: bool = false;
    118         let col = self.column;
    119 
    120         if let Some(action) = self
    121             .response
    122             .response
    123             .as_ref()
    124             .or(self.response.title_response.as_ref())
    125         {
    126             // start returning when we're finished posting
    127             match action {
    128                 RenderNavAction::Back => {
    129                     app.columns_mut(ctx.accounts)
    130                         .column_mut(col)
    131                         .router_mut()
    132                         .go_back();
    133                 }
    134 
    135                 RenderNavAction::RemoveColumn => {
    136                     let tl = app
    137                         .columns(ctx.accounts)
    138                         .find_timeline_for_column_index(col);
    139                     if let Some(timeline) = tl {
    140                         unsubscribe_timeline(ctx.ndb, timeline);
    141                     }
    142 
    143                     app.columns_mut(ctx.accounts).delete_column(col);
    144                     switching_occured = true;
    145                 }
    146 
    147                 RenderNavAction::PostAction(post_action) => {
    148                     let txn = Transaction::new(ctx.ndb).expect("txn");
    149                     let _ = post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts);
    150                     get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    151                         .column_mut(col)
    152                         .router_mut()
    153                         .go_back();
    154                 }
    155 
    156                 RenderNavAction::NoteAction(note_action) => {
    157                     let txn = Transaction::new(ctx.ndb).expect("txn");
    158 
    159                     note_action.execute_and_process_result(
    160                         ctx.ndb,
    161                         get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
    162                         col,
    163                         &mut app.timeline_cache,
    164                         ctx.note_cache,
    165                         ctx.pool,
    166                         &txn,
    167                         ctx.unknown_ids,
    168                     );
    169                 }
    170 
    171                 RenderNavAction::SwitchingAction(switching_action) => {
    172                     switching_occured = switching_action.process(&mut app.decks_cache, ctx);
    173                 }
    174                 RenderNavAction::ProfileAction(profile_action) => {
    175                     profile_action.process(
    176                         &mut app.view_state.pubkey_to_profile_state,
    177                         ctx.ndb,
    178                         ctx.pool,
    179                         get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    180                             .column_mut(col)
    181                             .router_mut(),
    182                     );
    183                 }
    184             }
    185         }
    186 
    187         if let Some(action) = self.response.action {
    188             match action {
    189                 NavAction::Returned => {
    190                     let r = app
    191                         .columns_mut(ctx.accounts)
    192                         .column_mut(col)
    193                         .router_mut()
    194                         .pop();
    195                     let txn = Transaction::new(ctx.ndb).expect("txn");
    196 
    197                     if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r {
    198                         match notedeck::note::root_note_id_from_selected_id(
    199                             ctx.ndb,
    200                             ctx.note_cache,
    201                             &txn,
    202                             id.bytes(),
    203                         ) {
    204                             Ok(root_id) => {
    205                                 if let Some(thread) =
    206                                     app.timeline_cache.threads.get_mut(root_id.bytes())
    207                                 {
    208                                     if let Some(sub) = &mut thread.subscription {
    209                                         sub.unsubscribe(ctx.ndb, ctx.pool);
    210                                     }
    211                                 }
    212                             }
    213 
    214                             Err(RootIdError::NoteNotFound) => {
    215                                 error!("thread returned: note not found for unsub??: {}", id.hex())
    216                             }
    217 
    218                             Err(RootIdError::NoRootId) => {
    219                                 error!("thread returned: note not found for unsub??: {}", id.hex())
    220                             }
    221                         }
    222                     } else if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r {
    223                         if let Some(profile) = app.timeline_cache.profiles.get_mut(pubkey.bytes()) {
    224                             if let Some(sub) = &mut profile.subscription {
    225                                 sub.unsubscribe(ctx.ndb, ctx.pool);
    226                             }
    227                         }
    228                     }
    229 
    230                     switching_occured = true;
    231                 }
    232 
    233                 NavAction::Navigated => {
    234                     let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut();
    235                     cur_router.navigating = false;
    236                     if cur_router.is_replacing() {
    237                         cur_router.remove_previous_routes();
    238                     }
    239                     switching_occured = true;
    240                 }
    241 
    242                 NavAction::Dragging => {}
    243                 NavAction::Returning => {}
    244                 NavAction::Resetting => {}
    245                 NavAction::Navigating => {}
    246             }
    247         }
    248 
    249         switching_occured
    250     }
    251 }
    252 
    253 fn render_nav_body(
    254     ui: &mut egui::Ui,
    255     app: &mut Damus,
    256     ctx: &mut AppContext<'_>,
    257     top: &Route,
    258     col: usize,
    259 ) -> Option<RenderNavAction> {
    260     match top {
    261         Route::Timeline(tlr) => render_timeline_route(
    262             ctx.ndb,
    263             get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
    264             &mut app.drafts,
    265             ctx.img_cache,
    266             ctx.unknown_ids,
    267             ctx.note_cache,
    268             &mut app.timeline_cache,
    269             ctx.accounts,
    270             *tlr,
    271             col,
    272             app.textmode,
    273             ui,
    274         ),
    275         Route::Accounts(amr) => {
    276             let mut action = render_accounts_route(
    277                 ui,
    278                 ctx.ndb,
    279                 col,
    280                 ctx.img_cache,
    281                 ctx.accounts,
    282                 &mut app.decks_cache,
    283                 &mut app.view_state.login,
    284                 *amr,
    285             );
    286             let txn = Transaction::new(ctx.ndb).expect("txn");
    287             action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
    288             action
    289                 .accounts_action
    290                 .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
    291         }
    292         Route::Relays => {
    293             let manager = RelayPoolManager::new(ctx.pool);
    294             RelayView::new(ctx.accounts, manager, &mut app.view_state.id_string_map).ui(ui);
    295             None
    296         }
    297         Route::ComposeNote => {
    298             let kp = ctx.accounts.get_selected_account()?.to_full()?;
    299             let draft = app.drafts.compose_mut();
    300 
    301             let txn = Transaction::new(ctx.ndb).expect("txn");
    302             let post_response = ui::PostView::new(
    303                 ctx.ndb,
    304                 draft,
    305                 PostType::New,
    306                 ctx.img_cache,
    307                 ctx.note_cache,
    308                 kp,
    309             )
    310             .ui(&txn, ui);
    311 
    312             post_response.action.map(Into::into)
    313         }
    314         Route::AddColumn(route) => {
    315             render_add_column_routes(ui, app, ctx, col, route);
    316 
    317             None
    318         }
    319         Route::Support => {
    320             SupportView::new(&mut app.support).show(ui);
    321             None
    322         }
    323         Route::NewDeck => {
    324             let id = ui.id().with("new-deck");
    325             let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
    326             let mut resp = None;
    327             if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) {
    328                 if let Some(cur_acc) = ctx.accounts.get_selected_account() {
    329                     app.decks_cache.add_deck(
    330                         cur_acc.pubkey,
    331                         Deck::new(config_resp.icon, config_resp.name),
    332                     );
    333 
    334                     // set new deck as active
    335                     let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache)
    336                         .decks()
    337                         .len()
    338                         - 1;
    339                     resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
    340                         DecksAction::Switch(cur_index),
    341                     )));
    342                 }
    343 
    344                 new_deck_state.clear();
    345                 get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    346                     .get_first_router()
    347                     .go_back();
    348             }
    349             resp
    350         }
    351         Route::EditDeck(index) => {
    352             let mut action = None;
    353             let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache)
    354                 .decks_mut()
    355                 .get_mut(*index)
    356                 .expect("index wasn't valid");
    357             let id = ui.id().with((
    358                 "edit-deck",
    359                 ctx.accounts.get_selected_account().map(|k| k.pubkey),
    360                 index,
    361             ));
    362             let deck_state = app
    363                 .view_state
    364                 .id_to_deck_state
    365                 .entry(id)
    366                 .or_insert_with(|| DeckState::from_deck(cur_deck));
    367             if let Some(resp) = EditDeckView::new(deck_state).ui(ui) {
    368                 match resp {
    369                     EditDeckResponse::Edit(configure_deck_response) => {
    370                         cur_deck.edit(configure_deck_response);
    371                     }
    372                     EditDeckResponse::Delete => {
    373                         action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
    374                             DecksAction::Removing(*index),
    375                         )));
    376                     }
    377                 }
    378                 get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
    379                     .get_first_router()
    380                     .go_back();
    381             }
    382 
    383             action
    384         }
    385         Route::EditProfile(pubkey) => {
    386             let mut action = None;
    387             if let Some(kp) = ctx.accounts.get_full(pubkey.bytes()) {
    388                 let state = app
    389                     .view_state
    390                     .pubkey_to_profile_state
    391                     .entry(*kp.pubkey)
    392                     .or_insert_with(|| {
    393                         let txn = Transaction::new(ctx.ndb).expect("txn");
    394                         if let Ok(record) = ctx.ndb.get_profile_by_pubkey(&txn, kp.pubkey.bytes()) {
    395                             ProfileState::from_profile(&record)
    396                         } else {
    397                             ProfileState::default()
    398                         }
    399                     });
    400                 if EditProfileView::new(state, ctx.img_cache).ui(ui) {
    401                     if let Some(taken_state) =
    402                         app.view_state.pubkey_to_profile_state.remove(kp.pubkey)
    403                     {
    404                         action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
    405                             SaveProfileChanges::new(kp.to_full(), taken_state),
    406                         )))
    407                     }
    408                 }
    409             } else {
    410                 error!("Pubkey in EditProfile route did not have an nsec attached in Accounts");
    411             }
    412             action
    413         }
    414     }
    415 }
    416 
    417 #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
    418 pub fn render_nav(
    419     col: usize,
    420     app: &mut Damus,
    421     ctx: &mut AppContext<'_>,
    422     ui: &mut egui::Ui,
    423 ) -> RenderNavResponse {
    424     let col_id = get_active_columns(ctx.accounts, &app.decks_cache).get_column_id_at_index(col);
    425     // TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly
    426 
    427     let nav_response = Nav::new(
    428         &app.columns(ctx.accounts)
    429             .column(col)
    430             .router()
    431             .routes()
    432             .clone(),
    433     )
    434     .navigating(
    435         app.columns_mut(ctx.accounts)
    436             .column_mut(col)
    437             .router_mut()
    438             .navigating,
    439     )
    440     .returning(
    441         app.columns_mut(ctx.accounts)
    442             .column_mut(col)
    443             .router_mut()
    444             .returning,
    445     )
    446     .id_source(egui::Id::new(col_id))
    447     .show_mut(ui, |ui, render_type, nav| match render_type {
    448         NavUiType::Title => NavTitle::new(
    449             ctx.ndb,
    450             ctx.img_cache,
    451             get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
    452             ctx.accounts.get_selected_account().map(|a| &a.pubkey),
    453             nav.routes(),
    454             col,
    455         )
    456         .show(ui),
    457         NavUiType::Body => render_nav_body(ui, app, ctx, nav.routes().last().expect("top"), col),
    458     });
    459 
    460     RenderNavResponse::new(col, nav_response)
    461 }
    462 
    463 fn unsubscribe_timeline(ndb: &mut Ndb, timeline: &Timeline) {
    464     if let Some(sub_id) = timeline.subscription {
    465         if let Err(e) = ndb.unsubscribe(sub_id) {
    466             error!("unsubscribe error: {}", e);
    467         } else {
    468             info!(
    469                 "successfully unsubscribed from timeline {} with sub id {}",
    470                 timeline.id,
    471                 sub_id.id()
    472             );
    473         }
    474     }
    475 }