notedeck

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

preview.rs (8850B)


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