notedeck

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

preview.rs (9963B)


      1 use crate::ui::ProfilePic;
      2 use crate::{colors, images, DisplayName};
      3 use egui::load::TexturePoll;
      4 use egui::{Frame, Label, RichText, Sense, Widget};
      5 use egui_extras::Size;
      6 use enostr::{NoteId, Pubkey};
      7 use nostrdb::{Ndb, ProfileRecord, Transaction};
      8 
      9 use notedeck::{DataPath, DataPathType, ImageCache, NotedeckTextStyle, UserAccount};
     10 
     11 pub struct ProfilePreview<'a, 'cache> {
     12     profile: &'a ProfileRecord<'a>,
     13     cache: &'cache mut ImageCache,
     14     banner_height: Size,
     15 }
     16 
     17 impl<'a, 'cache> ProfilePreview<'a, 'cache> {
     18     pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut ImageCache) -> Self {
     19         let banner_height = Size::exact(80.0);
     20         ProfilePreview {
     21             profile,
     22             cache,
     23             banner_height,
     24         }
     25     }
     26 
     27     pub fn banner_height(&mut self, size: Size) {
     28         self.banner_height = size;
     29     }
     30 
     31     fn banner_texture(
     32         ui: &mut egui::Ui,
     33         profile: &ProfileRecord<'_>,
     34     ) -> Option<egui::load::SizedTexture> {
     35         // TODO: cache banner
     36         let banner = profile.record().profile().and_then(|p| p.banner());
     37 
     38         if let Some(banner) = banner {
     39             let texture_load_res =
     40                 egui::Image::new(banner).load_for_size(ui.ctx(), ui.available_size());
     41             if let Ok(texture_poll) = texture_load_res {
     42                 match texture_poll {
     43                     TexturePoll::Pending { .. } => {}
     44                     TexturePoll::Ready { texture, .. } => return Some(texture),
     45                 }
     46             }
     47         }
     48 
     49         None
     50     }
     51 
     52     fn banner(ui: &mut egui::Ui, profile: &ProfileRecord<'_>) -> egui::Response {
     53         if let Some(texture) = Self::banner_texture(ui, profile) {
     54             images::aspect_fill(
     55                 ui,
     56                 Sense::hover(),
     57                 texture.id,
     58                 texture.size.x / texture.size.y,
     59             )
     60         } else {
     61             // TODO: default banner texture
     62             ui.label("")
     63         }
     64     }
     65 
     66     fn body(self, ui: &mut egui::Ui) {
     67         let padding = 12.0;
     68         crate::ui::padding(padding, ui, |ui| {
     69             let mut pfp_rect = ui.available_rect_before_wrap();
     70             let size = 80.0;
     71             pfp_rect.set_width(size);
     72             pfp_rect.set_height(size);
     73             let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0))));
     74 
     75             ui.put(
     76                 pfp_rect,
     77                 ProfilePic::new(self.cache, get_profile_url(Some(self.profile))).size(size),
     78             );
     79             ui.add(display_name_widget(
     80                 get_display_name(Some(self.profile)),
     81                 false,
     82             ));
     83             ui.add(about_section_widget(self.profile));
     84         });
     85     }
     86 }
     87 
     88 impl egui::Widget for ProfilePreview<'_, '_> {
     89     fn ui(self, ui: &mut egui::Ui) -> egui::Response {
     90         ui.vertical(|ui| {
     91             ui.add_sized([ui.available_size().x, 80.0], |ui: &mut egui::Ui| {
     92                 ProfilePreview::banner(ui, self.profile)
     93             });
     94 
     95             self.body(ui);
     96         })
     97         .response
     98     }
     99 }
    100 
    101 pub struct SimpleProfilePreview<'a, 'cache> {
    102     profile: Option<&'a ProfileRecord<'a>>,
    103     cache: &'cache mut ImageCache,
    104     is_nsec: bool,
    105 }
    106 
    107 impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
    108     pub fn new(
    109         profile: Option<&'a ProfileRecord<'a>>,
    110         cache: &'cache mut ImageCache,
    111         is_nsec: bool,
    112     ) -> Self {
    113         SimpleProfilePreview {
    114             profile,
    115             cache,
    116             is_nsec,
    117         }
    118     }
    119 }
    120 
    121 impl egui::Widget for SimpleProfilePreview<'_, '_> {
    122     fn ui(self, ui: &mut egui::Ui) -> egui::Response {
    123         Frame::none()
    124             .show(ui, |ui| {
    125                 ui.add(ProfilePic::new(self.cache, get_profile_url(self.profile)).size(48.0));
    126                 ui.vertical(|ui| {
    127                     ui.add(display_name_widget(get_display_name(self.profile), true));
    128                     if !self.is_nsec {
    129                         ui.add(
    130                             Label::new(
    131                                 RichText::new("Read only")
    132                                     .size(notedeck::fonts::get_font_size(
    133                                         ui.ctx(),
    134                                         &NotedeckTextStyle::Tiny,
    135                                     ))
    136                                     .color(ui.visuals().warn_fg_color),
    137                             )
    138                             .selectable(false),
    139                         );
    140                     }
    141                 });
    142             })
    143             .response
    144     }
    145 }
    146 
    147 mod previews {
    148     use super::*;
    149     use crate::test_data::test_profile_record;
    150     use crate::ui::{Preview, PreviewConfig, View};
    151 
    152     pub struct ProfilePreviewPreview<'a> {
    153         profile: ProfileRecord<'a>,
    154         cache: ImageCache,
    155     }
    156 
    157     impl ProfilePreviewPreview<'_> {
    158         pub fn new() -> Self {
    159             let profile = test_profile_record();
    160             let path = DataPath::new("previews")
    161                 .path(DataPathType::Cache)
    162                 .join(ImageCache::rel_dir());
    163             let cache = ImageCache::new(path);
    164             ProfilePreviewPreview { profile, cache }
    165         }
    166     }
    167 
    168     impl Default for ProfilePreviewPreview<'_> {
    169         fn default() -> Self {
    170             ProfilePreviewPreview::new()
    171         }
    172     }
    173 
    174     impl View for ProfilePreviewPreview<'_> {
    175         fn ui(&mut self, ui: &mut egui::Ui) {
    176             ProfilePreview::new(&self.profile, &mut self.cache).ui(ui);
    177         }
    178     }
    179 
    180     impl<'a> Preview for ProfilePreview<'a, '_> {
    181         /// A preview of the profile preview :D
    182         type Prev = ProfilePreviewPreview<'a>;
    183 
    184         fn preview(_cfg: PreviewConfig) -> Self::Prev {
    185             ProfilePreviewPreview::new()
    186         }
    187     }
    188 }
    189 
    190 pub fn get_display_name<'a>(profile: Option<&ProfileRecord<'a>>) -> DisplayName<'a> {
    191     if let Some(name) = profile.and_then(|p| crate::profile::get_profile_name(p)) {
    192         name
    193     } else {
    194         DisplayName::One("??")
    195     }
    196 }
    197 
    198 pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str {
    199     if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) {
    200         url
    201     } else {
    202         ProfilePic::no_pfp_url()
    203     }
    204 }
    205 
    206 pub fn get_profile_url_owned(profile: Option<ProfileRecord<'_>>) -> &str {
    207     if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) {
    208         url
    209     } else {
    210         ProfilePic::no_pfp_url()
    211     }
    212 }
    213 
    214 pub fn get_account_url<'a>(
    215     txn: &'a nostrdb::Transaction,
    216     ndb: &nostrdb::Ndb,
    217     account: Option<&UserAccount>,
    218 ) -> &'a str {
    219     if let Some(selected_account) = account {
    220         if let Ok(profile) = ndb.get_profile_by_pubkey(txn, selected_account.pubkey.bytes()) {
    221             get_profile_url_owned(Some(profile))
    222         } else {
    223             get_profile_url_owned(None)
    224         }
    225     } else {
    226         get_profile_url(None)
    227     }
    228 }
    229 
    230 fn display_name_widget(
    231     display_name: DisplayName<'_>,
    232     add_placeholder_space: bool,
    233 ) -> impl egui::Widget + '_ {
    234     move |ui: &mut egui::Ui| match display_name {
    235         DisplayName::One(n) => {
    236             let name_response = ui.add(
    237                 Label::new(RichText::new(n).text_style(NotedeckTextStyle::Heading3.text_style()))
    238                     .selectable(false),
    239             );
    240             if add_placeholder_space {
    241                 ui.add_space(16.0);
    242             }
    243             name_response
    244         }
    245 
    246         DisplayName::Both {
    247             display_name,
    248             username,
    249         } => {
    250             ui.add(
    251                 Label::new(
    252                     RichText::new(display_name)
    253                         .text_style(NotedeckTextStyle::Heading3.text_style()),
    254                 )
    255                 .selectable(false),
    256             );
    257 
    258             ui.add(
    259                 Label::new(
    260                     RichText::new(format!("@{}", username))
    261                         .size(12.0)
    262                         .color(colors::MID_GRAY),
    263                 )
    264                 .selectable(false),
    265             )
    266         }
    267     }
    268 }
    269 
    270 pub fn one_line_display_name_widget<'a>(
    271     visuals: &egui::Visuals,
    272     display_name: DisplayName<'a>,
    273     style: NotedeckTextStyle,
    274 ) -> impl egui::Widget + 'a {
    275     let text_style = style.text_style();
    276     let color = visuals.noninteractive().fg_stroke.color;
    277 
    278     move |ui: &mut egui::Ui| match display_name {
    279         DisplayName::One(n) => ui.label(RichText::new(n).text_style(text_style).color(color)),
    280 
    281         DisplayName::Both {
    282             display_name,
    283             username: _,
    284         } => ui.label(
    285             RichText::new(display_name)
    286                 .text_style(text_style)
    287                 .color(color),
    288         ),
    289     }
    290 }
    291 
    292 fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b
    293 where
    294     'b: 'a,
    295 {
    296     move |ui: &mut egui::Ui| {
    297         if let Some(about) = profile.record().profile().and_then(|p| p.about()) {
    298             ui.label(about)
    299         } else {
    300             // need any Response so we dont need an Option
    301             ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover())
    302         }
    303     }
    304 }
    305 
    306 fn get_display_name_as_string<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str {
    307     let display_name = get_display_name(profile);
    308     match display_name {
    309         DisplayName::One(n) => n,
    310         DisplayName::Both { display_name, .. } => display_name,
    311     }
    312 }
    313 
    314 pub fn get_profile_displayname_string<'a>(txn: &'a Transaction, ndb: &Ndb, pk: &Pubkey) -> &'a str {
    315     let profile = ndb.get_profile_by_pubkey(txn, pk.bytes()).ok();
    316     get_display_name_as_string(profile.as_ref())
    317 }
    318 
    319 pub fn get_note_users_displayname_string<'a>(
    320     txn: &'a Transaction,
    321     ndb: &Ndb,
    322     id: &NoteId,
    323 ) -> &'a str {
    324     let note = ndb.get_note_by_id(txn, id.bytes());
    325     let profile = if let Ok(note) = note {
    326         ndb.get_profile_by_pubkey(txn, note.pubkey()).ok()
    327     } else {
    328         None
    329     };
    330 
    331     get_display_name_as_string(profile.as_ref())
    332 }