notedeck

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

add_column.rs (14348B)


      1 use core::f32;
      2 use serde::{Deserialize, Serialize};
      3 use std::collections::HashMap;
      4 
      5 use egui::{
      6     pos2, vec2, Align, Color32, FontId, Id, ImageSource, Margin, Pos2, Rect, RichText, Separator,
      7     Ui, Vec2,
      8 };
      9 use nostrdb::Ndb;
     10 use tracing::error;
     11 
     12 use crate::{
     13     app_style::{get_font_size, NotedeckTextStyle},
     14     login_manager::AcquireKeyState,
     15     timeline::{PubkeySource, Timeline, TimelineKind},
     16     ui::anim::ICON_EXPANSION_MULTIPLE,
     17     user_account::UserAccount,
     18     Damus,
     19 };
     20 
     21 use super::{anim::AnimationHelper, padding};
     22 
     23 pub enum AddColumnResponse {
     24     Timeline(Timeline),
     25     UndecidedNotification,
     26     ExternalNotification,
     27 }
     28 
     29 pub enum NotificationColumnType {
     30     Home,
     31     External,
     32 }
     33 
     34 #[derive(Clone, Debug)]
     35 enum AddColumnOption {
     36     Universe,
     37     UndecidedNotification,
     38     ExternalNotification,
     39     Notification(PubkeySource),
     40     Home(PubkeySource),
     41 }
     42 
     43 #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)]
     44 pub enum AddColumnRoute {
     45     Base,
     46     UndecidedNotification,
     47     ExternalNotification,
     48 }
     49 
     50 impl AddColumnOption {
     51     pub fn take_as_response(
     52         self,
     53         ndb: &Ndb,
     54         cur_account: Option<&UserAccount>,
     55     ) -> Option<AddColumnResponse> {
     56         match self {
     57             AddColumnOption::Universe => TimelineKind::Universe
     58                 .into_timeline(ndb, None)
     59                 .map(AddColumnResponse::Timeline),
     60             AddColumnOption::Notification(pubkey) => TimelineKind::Notifications(pubkey)
     61                 .into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes()))
     62                 .map(AddColumnResponse::Timeline),
     63             AddColumnOption::UndecidedNotification => {
     64                 Some(AddColumnResponse::UndecidedNotification)
     65             }
     66             AddColumnOption::Home(pubkey) => {
     67                 let tlk = TimelineKind::contact_list(pubkey);
     68                 tlk.into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes()))
     69                     .map(AddColumnResponse::Timeline)
     70             }
     71             AddColumnOption::ExternalNotification => Some(AddColumnResponse::ExternalNotification),
     72         }
     73     }
     74 }
     75 
     76 pub struct AddColumnView<'a> {
     77     key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
     78     ndb: &'a Ndb,
     79     cur_account: Option<&'a UserAccount>,
     80 }
     81 
     82 impl<'a> AddColumnView<'a> {
     83     pub fn new(
     84         key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
     85         ndb: &'a Ndb,
     86         cur_account: Option<&'a UserAccount>,
     87     ) -> Self {
     88         Self {
     89             key_state_map,
     90             ndb,
     91             cur_account,
     92         }
     93     }
     94 
     95     pub fn ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
     96         let mut selected_option: Option<AddColumnResponse> = None;
     97         for column_option_data in self.get_base_options() {
     98             let option = column_option_data.option.clone();
     99             if self.column_option_ui(ui, column_option_data).clicked() {
    100                 selected_option = option.take_as_response(self.ndb, self.cur_account);
    101             }
    102 
    103             ui.add(Separator::default().spacing(0.0));
    104         }
    105 
    106         selected_option
    107     }
    108 
    109     fn notifications_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
    110         let mut selected_option: Option<AddColumnResponse> = None;
    111         for column_option_data in self.get_notifications_options() {
    112             let option = column_option_data.option.clone();
    113             if self.column_option_ui(ui, column_option_data).clicked() {
    114                 selected_option = option.take_as_response(self.ndb, self.cur_account);
    115             }
    116 
    117             ui.add(Separator::default().spacing(0.0));
    118         }
    119 
    120         selected_option
    121     }
    122 
    123     fn external_notification_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
    124         padding(16.0, ui, |ui| {
    125             let id = ui.id().with("external_notif");
    126             let key_state = self.key_state_map.entry(id).or_default();
    127 
    128             let text_edit = key_state.get_acquire_textedit(|text| {
    129                 egui::TextEdit::singleline(text)
    130                     .hint_text(
    131                         RichText::new("Enter the user's key (npub, hex, nip05) here...")
    132                             .text_style(NotedeckTextStyle::Body.text_style()),
    133                     )
    134                     .vertical_align(Align::Center)
    135                     .desired_width(f32::INFINITY)
    136                     .min_size(Vec2::new(0.0, 40.0))
    137                     .margin(Margin::same(12.0))
    138             });
    139 
    140             ui.add(text_edit);
    141 
    142             if ui.button("Add").clicked() {
    143                 key_state.apply_acquire();
    144             }
    145 
    146             if key_state.is_awaiting_network() {
    147                 ui.spinner();
    148             }
    149 
    150             if let Some(error) = key_state.check_for_error() {
    151                 error!("acquire key error: {}", error);
    152                 ui.colored_label(
    153                     Color32::RED,
    154                     "Please enter a valid npub, public hex key or nip05",
    155                 );
    156             }
    157 
    158             if let Some(keypair) = key_state.check_for_successful_login() {
    159                 key_state.should_create_new();
    160                 AddColumnOption::Notification(PubkeySource::Explicit(keypair.pubkey))
    161                     .take_as_response(self.ndb, self.cur_account)
    162             } else {
    163                 None
    164             }
    165         })
    166         .inner
    167     }
    168 
    169     fn column_option_ui(&mut self, ui: &mut Ui, data: ColumnOptionData) -> egui::Response {
    170         let icon_padding = 8.0;
    171         let min_icon_width = 32.0;
    172         let height_padding = 12.0;
    173         let max_width = ui.available_width();
    174         let title_style = NotedeckTextStyle::Body;
    175         let desc_style = NotedeckTextStyle::Button;
    176         let title_min_font_size = get_font_size(ui.ctx(), &title_style);
    177         let desc_min_font_size = get_font_size(ui.ctx(), &desc_style);
    178 
    179         let max_height = {
    180             let max_wrap_width =
    181                 max_width - ((icon_padding * 2.0) + (min_icon_width * ICON_EXPANSION_MULTIPLE));
    182             let title_max_font = FontId::new(
    183                 title_min_font_size * ICON_EXPANSION_MULTIPLE,
    184                 title_style.font_family(),
    185             );
    186             let desc_max_font = FontId::new(
    187                 desc_min_font_size * ICON_EXPANSION_MULTIPLE,
    188                 desc_style.font_family(),
    189             );
    190             let max_desc_galley = ui.fonts(|f| {
    191                 f.layout(
    192                     data.description.to_string(),
    193                     desc_max_font,
    194                     Color32::WHITE,
    195                     max_wrap_width,
    196                 )
    197             });
    198 
    199             let max_title_galley = ui.fonts(|f| {
    200                 f.layout(
    201                     data.title.to_string(),
    202                     title_max_font,
    203                     Color32::WHITE,
    204                     max_wrap_width,
    205                 )
    206             });
    207 
    208             let desc_font_max_size = max_desc_galley.rect.height();
    209             let title_font_max_size = max_title_galley.rect.height();
    210             title_font_max_size + desc_font_max_size + (2.0 * height_padding)
    211         };
    212 
    213         let helper = AnimationHelper::new(ui, data.title, vec2(max_width, max_height));
    214         let animation_rect = helper.get_animation_rect();
    215 
    216         let cur_icon_width = helper.scale_1d_pos(min_icon_width);
    217         let painter = ui.painter_at(animation_rect);
    218 
    219         let cur_icon_size = vec2(cur_icon_width, cur_icon_width);
    220         let cur_icon_x_pos = animation_rect.left() + (icon_padding) + (cur_icon_width / 2.0);
    221 
    222         let title_cur_font = FontId::new(
    223             helper.scale_1d_pos(title_min_font_size),
    224             title_style.font_family(),
    225         );
    226 
    227         let desc_cur_font = FontId::new(
    228             helper.scale_1d_pos(desc_min_font_size),
    229             desc_style.font_family(),
    230         );
    231 
    232         let wrap_width = max_width - (cur_icon_width + (icon_padding * 2.0));
    233         let text_color = ui.ctx().style().visuals.text_color();
    234         let fallback_color = ui.ctx().style().visuals.weak_text_color();
    235 
    236         let title_galley = painter.layout(
    237             data.title.to_string(),
    238             title_cur_font,
    239             text_color,
    240             wrap_width,
    241         );
    242         let desc_galley = painter.layout(
    243             data.description.to_string(),
    244             desc_cur_font,
    245             text_color,
    246             wrap_width,
    247         );
    248 
    249         let galley_heights = title_galley.rect.height() + desc_galley.rect.height();
    250 
    251         let cur_height_padding = (animation_rect.height() - galley_heights) / 2.0;
    252         let corner_x_pos = cur_icon_x_pos + (cur_icon_width / 2.0) + icon_padding;
    253         let title_corner_pos = Pos2::new(corner_x_pos, animation_rect.top() + cur_height_padding);
    254         let desc_corner_pos = Pos2::new(
    255             corner_x_pos,
    256             title_corner_pos.y + title_galley.rect.height(),
    257         );
    258 
    259         let icon_cur_y = animation_rect.top() + cur_height_padding + (galley_heights / 2.0);
    260         let icon_img = egui::Image::new(data.icon).fit_to_exact_size(cur_icon_size);
    261         let icon_rect = Rect::from_center_size(pos2(cur_icon_x_pos, icon_cur_y), cur_icon_size);
    262 
    263         icon_img.paint_at(ui, icon_rect);
    264         painter.galley(title_corner_pos, title_galley, fallback_color);
    265         painter.galley(desc_corner_pos, desc_galley, fallback_color);
    266 
    267         helper.take_animation_response()
    268     }
    269 
    270     fn get_base_options(&self) -> Vec<ColumnOptionData> {
    271         let mut vec = Vec::new();
    272         vec.push(ColumnOptionData {
    273             title: "Universe",
    274             description: "See the whole nostr universe",
    275             icon: egui::include_image!("../../assets/icons/universe_icon_dark_4x.png"),
    276             option: AddColumnOption::Universe,
    277         });
    278 
    279         if let Some(acc) = self.cur_account {
    280             let source = PubkeySource::Explicit(acc.pubkey);
    281 
    282             vec.push(ColumnOptionData {
    283                 title: "Home timeline",
    284                 description: "See recommended notes first",
    285                 icon: egui::include_image!("../../assets/icons/home_icon_dark_4x.png"),
    286                 option: AddColumnOption::Home(source.clone()),
    287             });
    288         }
    289         vec.push(ColumnOptionData {
    290             title: "Notifications",
    291             description: "Stay up to date with notifications and mentions",
    292             icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"),
    293             option: AddColumnOption::UndecidedNotification,
    294         });
    295 
    296         vec
    297     }
    298 
    299     fn get_notifications_options(&self) -> Vec<ColumnOptionData> {
    300         let mut vec = Vec::new();
    301 
    302         if let Some(acc) = self.cur_account {
    303             let source = if acc.secret_key.is_some() {
    304                 PubkeySource::DeckAuthor
    305             } else {
    306                 PubkeySource::Explicit(acc.pubkey)
    307             };
    308 
    309             vec.push(ColumnOptionData {
    310                 title: "Your Notifications",
    311                 description: "Stay up to date with your notifications and mentions",
    312                 icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"),
    313                 option: AddColumnOption::Notification(source),
    314             });
    315         }
    316 
    317         vec.push(ColumnOptionData {
    318             title: "Someone else's Notifications",
    319             description: "Stay up to date with someone else's notifications and mentions",
    320             icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"),
    321             option: AddColumnOption::ExternalNotification,
    322         });
    323 
    324         vec
    325     }
    326 }
    327 
    328 struct ColumnOptionData {
    329     title: &'static str,
    330     description: &'static str,
    331     icon: ImageSource<'static>,
    332     option: AddColumnOption,
    333 }
    334 
    335 pub fn render_add_column_routes(
    336     ui: &mut egui::Ui,
    337     app: &mut Damus,
    338     col: usize,
    339     route: &AddColumnRoute,
    340 ) {
    341     let resp = match route {
    342         AddColumnRoute::Base => AddColumnView::new(
    343             &mut app.view_state.id_state_map,
    344             &app.ndb,
    345             app.accounts.get_selected_account(),
    346         )
    347         .ui(ui),
    348         AddColumnRoute::UndecidedNotification => AddColumnView::new(
    349             &mut app.view_state.id_state_map,
    350             &app.ndb,
    351             app.accounts.get_selected_account(),
    352         )
    353         .notifications_ui(ui),
    354         AddColumnRoute::ExternalNotification => AddColumnView::new(
    355             &mut app.view_state.id_state_map,
    356             &app.ndb,
    357             app.accounts.get_selected_account(),
    358         )
    359         .external_notification_ui(ui),
    360     };
    361 
    362     if let Some(resp) = resp {
    363         match resp {
    364             AddColumnResponse::Timeline(mut timeline) => {
    365                 crate::timeline::setup_new_timeline(
    366                     &mut timeline,
    367                     &app.ndb,
    368                     &mut app.subscriptions,
    369                     &mut app.pool,
    370                     &mut app.note_cache,
    371                     app.since_optimize,
    372                 );
    373                 app.columns_mut().add_timeline_to_column(col, timeline);
    374             }
    375             AddColumnResponse::UndecidedNotification => {
    376                 app.columns_mut().column_mut(col).router_mut().route_to(
    377                     crate::route::Route::AddColumn(AddColumnRoute::UndecidedNotification),
    378                 );
    379             }
    380             AddColumnResponse::ExternalNotification => {
    381                 app.columns_mut().column_mut(col).router_mut().route_to(
    382                     crate::route::Route::AddColumn(AddColumnRoute::ExternalNotification),
    383                 );
    384             }
    385         };
    386     }
    387 }
    388 
    389 mod preview {
    390     use crate::{
    391         test_data,
    392         ui::{Preview, PreviewConfig, View},
    393         Damus,
    394     };
    395 
    396     use super::AddColumnView;
    397 
    398     pub struct AddColumnPreview {
    399         app: Damus,
    400     }
    401 
    402     impl AddColumnPreview {
    403         fn new() -> Self {
    404             let app = test_data::test_app();
    405 
    406             AddColumnPreview { app }
    407         }
    408     }
    409 
    410     impl View for AddColumnPreview {
    411         fn ui(&mut self, ui: &mut egui::Ui) {
    412             AddColumnView::new(
    413                 &mut self.app.view_state.id_state_map,
    414                 &self.app.ndb,
    415                 self.app.accounts.get_selected_account(),
    416             )
    417             .ui(ui);
    418         }
    419     }
    420 
    421     impl<'a> Preview for AddColumnView<'a> {
    422         type Prev = AddColumnPreview;
    423 
    424         fn preview(_cfg: PreviewConfig) -> Self::Prev {
    425             AddColumnPreview::new()
    426         }
    427     }
    428 }