notedeck

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

mod.rs (15518B)


      1 pub mod contents;
      2 pub mod options;
      3 pub mod post;
      4 pub mod reply;
      5 
      6 pub use contents::NoteContents;
      7 pub use options::NoteOptions;
      8 pub use post::{PostAction, PostResponse, PostView};
      9 pub use reply::PostReplyView;
     10 
     11 use crate::{actionbar::BarAction, colors, notecache::CachedNote, ui, ui::View, Damus};
     12 use egui::{Label, RichText, Sense};
     13 use nostrdb::{Note, NoteKey, NoteReply, Transaction};
     14 
     15 pub struct NoteView<'a> {
     16     app: &'a mut Damus,
     17     note: &'a nostrdb::Note<'a>,
     18     flags: NoteOptions,
     19 }
     20 
     21 pub struct NoteResponse {
     22     pub response: egui::Response,
     23     pub action: Option<BarAction>,
     24 }
     25 
     26 impl<'a> View for NoteView<'a> {
     27     fn ui(&mut self, ui: &mut egui::Ui) {
     28         self.show(ui);
     29     }
     30 }
     31 
     32 fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: &mut Damus) {
     33     #[cfg(feature = "profiling")]
     34     puffin::profile_function!();
     35 
     36     let size = 10.0;
     37     let selectable = false;
     38 
     39     ui.add(
     40         Label::new(
     41             RichText::new("replying to")
     42                 .size(size)
     43                 .color(colors::GRAY_SECONDARY),
     44         )
     45         .selectable(selectable),
     46     );
     47 
     48     let reply = if let Some(reply) = note_reply.reply() {
     49         reply
     50     } else {
     51         return;
     52     };
     53 
     54     let reply_note = if let Ok(reply_note) = app.ndb.get_note_by_id(txn, reply.id) {
     55         reply_note
     56     } else {
     57         ui.add(
     58             Label::new(
     59                 RichText::new("a note")
     60                     .size(size)
     61                     .color(colors::GRAY_SECONDARY),
     62             )
     63             .selectable(selectable),
     64         );
     65         return;
     66     };
     67 
     68     if note_reply.is_reply_to_root() {
     69         // We're replying to the root, let's show this
     70         ui.add(
     71             ui::Mention::new(app, txn, reply_note.pubkey())
     72                 .size(size)
     73                 .selectable(selectable),
     74         );
     75         ui.add(
     76             Label::new(
     77                 RichText::new("'s note")
     78                     .size(size)
     79                     .color(colors::GRAY_SECONDARY),
     80             )
     81             .selectable(selectable),
     82         );
     83     } else if let Some(root) = note_reply.root() {
     84         // replying to another post in a thread, not the root
     85 
     86         if let Ok(root_note) = app.ndb.get_note_by_id(txn, root.id) {
     87             if root_note.pubkey() == reply_note.pubkey() {
     88                 // simply "replying to bob's note" when replying to bob in his thread
     89                 ui.add(
     90                     ui::Mention::new(app, txn, reply_note.pubkey())
     91                         .size(size)
     92                         .selectable(selectable),
     93                 );
     94                 ui.add(
     95                     Label::new(
     96                         RichText::new("'s note")
     97                             .size(size)
     98                             .color(colors::GRAY_SECONDARY),
     99                     )
    100                     .selectable(selectable),
    101                 );
    102             } else {
    103                 // replying to bob in alice's thread
    104 
    105                 ui.add(
    106                     ui::Mention::new(app, txn, reply_note.pubkey())
    107                         .size(size)
    108                         .selectable(selectable),
    109                 );
    110                 ui.add(
    111                     Label::new(RichText::new("in").size(size).color(colors::GRAY_SECONDARY))
    112                         .selectable(selectable),
    113                 );
    114                 ui.add(
    115                     ui::Mention::new(app, txn, root_note.pubkey())
    116                         .size(size)
    117                         .selectable(selectable),
    118                 );
    119                 ui.add(
    120                     Label::new(
    121                         RichText::new("'s thread")
    122                             .size(size)
    123                             .color(colors::GRAY_SECONDARY),
    124                     )
    125                     .selectable(selectable),
    126                 );
    127             }
    128         } else {
    129             ui.add(
    130                 ui::Mention::new(app, txn, reply_note.pubkey())
    131                     .size(size)
    132                     .selectable(selectable),
    133             );
    134             ui.add(
    135                 Label::new(
    136                     RichText::new("in someone's thread")
    137                         .size(size)
    138                         .color(colors::GRAY_SECONDARY),
    139                 )
    140                 .selectable(selectable),
    141             );
    142         }
    143     }
    144 }
    145 
    146 impl<'a> NoteView<'a> {
    147     pub fn new(app: &'a mut Damus, note: &'a nostrdb::Note<'a>) -> Self {
    148         let flags = NoteOptions::actionbar | NoteOptions::note_previews;
    149         Self { app, note, flags }
    150     }
    151 
    152     pub fn actionbar(mut self, enable: bool) -> Self {
    153         self.options_mut().set_actionbar(enable);
    154         self
    155     }
    156 
    157     pub fn small_pfp(mut self, enable: bool) -> Self {
    158         self.options_mut().set_small_pfp(enable);
    159         self
    160     }
    161 
    162     pub fn medium_pfp(mut self, enable: bool) -> Self {
    163         self.options_mut().set_medium_pfp(enable);
    164         self
    165     }
    166 
    167     pub fn note_previews(mut self, enable: bool) -> Self {
    168         self.options_mut().set_note_previews(enable);
    169         self
    170     }
    171 
    172     pub fn selectable_text(mut self, enable: bool) -> Self {
    173         self.options_mut().set_selectable_text(enable);
    174         self
    175     }
    176 
    177     pub fn wide(mut self, enable: bool) -> Self {
    178         self.options_mut().set_wide(enable);
    179         self
    180     }
    181 
    182     pub fn options(&self) -> NoteOptions {
    183         self.flags
    184     }
    185 
    186     pub fn options_mut(&mut self) -> &mut NoteOptions {
    187         &mut self.flags
    188     }
    189 
    190     fn textmode_ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
    191         let note_key = self.note.key().expect("todo: implement non-db notes");
    192         let txn = self.note.txn().expect("todo: implement non-db notes");
    193 
    194         ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
    195             let profile = self.app.ndb.get_profile_by_pubkey(txn, self.note.pubkey());
    196 
    197             //ui.horizontal(|ui| {
    198             ui.spacing_mut().item_spacing.x = 2.0;
    199 
    200             let cached_note = self
    201                 .app
    202                 .note_cache_mut()
    203                 .cached_note_or_insert_mut(note_key, self.note);
    204 
    205             let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0));
    206             ui.allocate_rect(rect, Sense::hover());
    207             ui.put(rect, |ui: &mut egui::Ui| {
    208                 render_reltime(ui, cached_note, false).response
    209             });
    210             let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0));
    211             ui.allocate_rect(rect, Sense::hover());
    212             ui.put(rect, |ui: &mut egui::Ui| {
    213                 ui.add(
    214                     ui::Username::new(profile.as_ref().ok(), self.note.pubkey())
    215                         .abbreviated(6)
    216                         .pk_colored(true),
    217                 )
    218             });
    219 
    220             ui.add(NoteContents::new(
    221                 self.app, txn, self.note, note_key, self.flags,
    222             ));
    223             //});
    224         })
    225         .response
    226     }
    227 
    228     pub fn expand_size() -> f32 {
    229         5.0
    230     }
    231 
    232     fn pfp(
    233         &mut self,
    234         note_key: NoteKey,
    235         profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
    236         ui: &mut egui::Ui,
    237     ) {
    238         if !self.options().has_wide() {
    239             ui.spacing_mut().item_spacing.x = 16.0;
    240         } else {
    241             ui.spacing_mut().item_spacing.x = 4.0;
    242         }
    243 
    244         let pfp_size = self.options().pfp_size();
    245 
    246         match profile
    247             .as_ref()
    248             .ok()
    249             .and_then(|p| p.record().profile()?.picture())
    250         {
    251             // these have different lifetimes and types,
    252             // so the calls must be separate
    253             Some(pic) => {
    254                 let anim_speed = 0.05;
    255                 let profile_key = profile.as_ref().unwrap().record().note_key();
    256                 let note_key = note_key.as_u64();
    257 
    258                 if self.app.is_mobile() {
    259                     ui.add(ui::ProfilePic::new(&mut self.app.img_cache, pic));
    260                 } else {
    261                     let (rect, size, _resp) = ui::anim::hover_expand(
    262                         ui,
    263                         egui::Id::new((profile_key, note_key)),
    264                         pfp_size,
    265                         ui::NoteView::expand_size(),
    266                         anim_speed,
    267                     );
    268 
    269                     ui.put(
    270                         rect,
    271                         ui::ProfilePic::new(&mut self.app.img_cache, pic).size(size),
    272                     )
    273                     .on_hover_ui_at_pointer(|ui| {
    274                         ui.set_max_width(300.0);
    275                         ui.add(ui::ProfilePreview::new(
    276                             profile.as_ref().unwrap(),
    277                             &mut self.app.img_cache,
    278                         ));
    279                     });
    280                 }
    281             }
    282             None => {
    283                 ui.add(
    284                     ui::ProfilePic::new(&mut self.app.img_cache, ui::ProfilePic::no_pfp_url())
    285                         .size(pfp_size),
    286                 );
    287             }
    288         }
    289     }
    290 
    291     pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse {
    292         if self.app.textmode {
    293             NoteResponse {
    294                 response: self.textmode_ui(ui),
    295                 action: None,
    296             }
    297         } else {
    298             self.show_standard(ui)
    299         }
    300     }
    301 
    302     fn note_header(
    303         ui: &mut egui::Ui,
    304         app: &mut Damus,
    305         note: &Note,
    306         profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
    307     ) -> egui::Response {
    308         let note_key = note.key().unwrap();
    309 
    310         ui.horizontal(|ui| {
    311             ui.spacing_mut().item_spacing.x = 2.0;
    312             ui.add(ui::Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20));
    313 
    314             let cached_note = app
    315                 .note_cache_mut()
    316                 .cached_note_or_insert_mut(note_key, note);
    317             render_reltime(ui, cached_note, true);
    318         })
    319         .response
    320     }
    321 
    322     fn show_standard(&mut self, ui: &mut egui::Ui) -> NoteResponse {
    323         #[cfg(feature = "profiling")]
    324         puffin::profile_function!();
    325         let note_key = self.note.key().expect("todo: support non-db notes");
    326         let txn = self.note.txn().expect("todo: support non-db notes");
    327         let mut note_action: Option<BarAction> = None;
    328         let profile = self.app.ndb.get_profile_by_pubkey(txn, self.note.pubkey());
    329 
    330         // wide design
    331         let response = if self.options().has_wide() {
    332             ui.horizontal(|ui| {
    333                 self.pfp(note_key, &profile, ui);
    334 
    335                 let size = ui.available_size();
    336                 ui.vertical(|ui| {
    337                     ui.add_sized([size.x, self.options().pfp_size()], |ui: &mut egui::Ui| {
    338                         ui.horizontal_centered(|ui| {
    339                             NoteView::note_header(ui, self.app, self.note, &profile);
    340                         })
    341                         .response
    342                     });
    343 
    344                     let note_reply = self
    345                         .app
    346                         .note_cache_mut()
    347                         .cached_note_or_insert_mut(note_key, self.note)
    348                         .reply
    349                         .borrow(self.note.tags());
    350 
    351                     if note_reply.reply().is_some() {
    352                         ui.horizontal(|ui| {
    353                             reply_desc(ui, txn, &note_reply, self.app);
    354                         });
    355                     }
    356                 });
    357             });
    358 
    359             let resp = ui.add(NoteContents::new(
    360                 self.app,
    361                 txn,
    362                 self.note,
    363                 note_key,
    364                 self.options(),
    365             ));
    366 
    367             if self.options().has_actionbar() {
    368                 note_action = render_note_actionbar(ui, note_key).inner;
    369             }
    370 
    371             resp
    372         } else {
    373             // main design
    374             ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
    375                 self.pfp(note_key, &profile, ui);
    376 
    377                 ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
    378                     NoteView::note_header(ui, self.app, self.note, &profile);
    379 
    380                     ui.horizontal(|ui| {
    381                         ui.spacing_mut().item_spacing.x = 2.0;
    382 
    383                         let note_reply = self
    384                             .app
    385                             .note_cache_mut()
    386                             .cached_note_or_insert_mut(note_key, self.note)
    387                             .reply
    388                             .borrow(self.note.tags());
    389 
    390                         if note_reply.reply().is_some() {
    391                             reply_desc(ui, txn, &note_reply, self.app);
    392                         }
    393                     });
    394 
    395                     ui.add(NoteContents::new(
    396                         self.app,
    397                         txn,
    398                         self.note,
    399                         note_key,
    400                         self.options(),
    401                     ));
    402 
    403                     if self.options().has_actionbar() {
    404                         note_action = render_note_actionbar(ui, note_key).inner;
    405                     }
    406                 });
    407             })
    408             .response
    409         };
    410 
    411         NoteResponse {
    412             response,
    413             action: note_action,
    414         }
    415     }
    416 }
    417 
    418 fn render_note_actionbar(
    419     ui: &mut egui::Ui,
    420     note_key: NoteKey,
    421 ) -> egui::InnerResponse<Option<BarAction>> {
    422     ui.horizontal(|ui| {
    423         let reply_resp = reply_button(ui, note_key);
    424         let thread_resp = thread_button(ui, note_key);
    425 
    426         if reply_resp.clicked() {
    427             Some(BarAction::Reply)
    428         } else if thread_resp.clicked() {
    429             Some(BarAction::OpenThread)
    430         } else {
    431             None
    432         }
    433     })
    434 }
    435 
    436 fn secondary_label(ui: &mut egui::Ui, s: impl Into<String>) {
    437     ui.add(Label::new(
    438         RichText::new(s).size(10.0).color(colors::GRAY_SECONDARY),
    439     ));
    440 }
    441 
    442 fn render_reltime(
    443     ui: &mut egui::Ui,
    444     note_cache: &mut CachedNote,
    445     before: bool,
    446 ) -> egui::InnerResponse<()> {
    447     #[cfg(feature = "profiling")]
    448     puffin::profile_function!();
    449 
    450     ui.horizontal(|ui| {
    451         if before {
    452             secondary_label(ui, "⋅");
    453         }
    454 
    455         secondary_label(ui, note_cache.reltime_str_mut());
    456 
    457         if !before {
    458             secondary_label(ui, "⋅");
    459         }
    460     })
    461 }
    462 
    463 fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
    464     let img_data = if ui.style().visuals.dark_mode {
    465         egui::include_image!("../../../assets/icons/reply.png")
    466     } else {
    467         egui::include_image!("../../../assets/icons/reply-dark.png")
    468     };
    469 
    470     let (rect, size, resp) =
    471         ui::anim::hover_expand_small(ui, ui.id().with(("reply_anim", note_key)));
    472 
    473     // align rect to note contents
    474     let expand_size = 5.0; // from hover_expand_small
    475     let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
    476 
    477     let put_resp = ui.put(rect, egui::Image::new(img_data).max_width(size));
    478 
    479     resp.union(put_resp)
    480 }
    481 
    482 fn thread_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
    483     let id = ui.id().with(("thread_anim", note_key));
    484     let size = 8.0;
    485     let expand_size = 5.0;
    486     let anim_speed = 0.05;
    487 
    488     let (rect, size, resp) = ui::anim::hover_expand(ui, id, size, expand_size, anim_speed);
    489 
    490     let color = if ui.style().visuals.dark_mode {
    491         egui::Color32::WHITE
    492     } else {
    493         egui::Color32::BLACK
    494     };
    495 
    496     ui.painter_at(rect).circle_stroke(
    497         rect.center(),
    498         (size - 1.0) / 2.0,
    499         egui::Stroke::new(1.0, color),
    500     );
    501 
    502     resp
    503 }