notedeck

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

post.rs (9311B)


      1 use crate::draft::{Draft, Drafts};
      2 use crate::post::NewPost;
      3 use crate::ui::{self, Preview, PreviewConfig, View};
      4 use crate::Result;
      5 use egui::widgets::text_edit::TextEdit;
      6 use egui::{Frame, Layout};
      7 use enostr::{FilledKeypair, FullKeypair, NoteId, RelayPool};
      8 use nostrdb::{Config, Ndb, Transaction};
      9 use tracing::info;
     10 
     11 use notedeck::{ImageCache, NoteCache};
     12 
     13 use super::contents::render_note_preview;
     14 
     15 pub struct PostView<'a> {
     16     ndb: &'a Ndb,
     17     draft: &'a mut Draft,
     18     post_type: PostType,
     19     img_cache: &'a mut ImageCache,
     20     note_cache: &'a mut NoteCache,
     21     poster: FilledKeypair<'a>,
     22     id_source: Option<egui::Id>,
     23 }
     24 
     25 #[derive(Clone)]
     26 pub enum PostType {
     27     New,
     28     Quote(NoteId),
     29     Reply(NoteId),
     30 }
     31 
     32 pub struct PostAction {
     33     post_type: PostType,
     34     post: NewPost,
     35 }
     36 
     37 impl PostAction {
     38     pub fn new(post_type: PostType, post: NewPost) -> Self {
     39         PostAction { post_type, post }
     40     }
     41 
     42     pub fn execute(
     43         &self,
     44         ndb: &Ndb,
     45         txn: &Transaction,
     46         pool: &mut RelayPool,
     47         drafts: &mut Drafts,
     48     ) -> Result<()> {
     49         let seckey = self.post.account.secret_key.to_secret_bytes();
     50 
     51         let note = match self.post_type {
     52             PostType::New => self.post.to_note(&seckey),
     53 
     54             PostType::Reply(target) => {
     55                 let replying_to = ndb.get_note_by_id(txn, target.bytes())?;
     56                 self.post.to_reply(&seckey, &replying_to)
     57             }
     58 
     59             PostType::Quote(target) => {
     60                 let quoting = ndb.get_note_by_id(txn, target.bytes())?;
     61                 self.post.to_quote(&seckey, &quoting)
     62             }
     63         };
     64 
     65         let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
     66         info!("sending {}", raw_msg);
     67         pool.send(&enostr::ClientMessage::raw(raw_msg));
     68         drafts.get_from_post_type(&self.post_type).clear();
     69 
     70         Ok(())
     71     }
     72 }
     73 
     74 pub struct PostResponse {
     75     pub action: Option<PostAction>,
     76     pub edit_response: egui::Response,
     77 }
     78 
     79 impl<'a> PostView<'a> {
     80     pub fn new(
     81         ndb: &'a Ndb,
     82         draft: &'a mut Draft,
     83         post_type: PostType,
     84         img_cache: &'a mut ImageCache,
     85         note_cache: &'a mut NoteCache,
     86         poster: FilledKeypair<'a>,
     87     ) -> Self {
     88         let id_source: Option<egui::Id> = None;
     89         PostView {
     90             ndb,
     91             draft,
     92             img_cache,
     93             note_cache,
     94             poster,
     95             id_source,
     96             post_type,
     97         }
     98     }
     99 
    100     pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self {
    101         self.id_source = Some(egui::Id::new(id_source));
    102         self
    103     }
    104 
    105     fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
    106         ui.spacing_mut().item_spacing.x = 12.0;
    107 
    108         let pfp_size = 24.0;
    109 
    110         // TODO: refactor pfp control to do all of this for us
    111         let poster_pfp = self
    112             .ndb
    113             .get_profile_by_pubkey(txn, self.poster.pubkey.bytes())
    114             .as_ref()
    115             .ok()
    116             .and_then(|p| Some(ui::ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size)));
    117 
    118         if let Some(pfp) = poster_pfp {
    119             ui.add(pfp);
    120         } else {
    121             ui.add(
    122                 ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size),
    123             );
    124         }
    125 
    126         let response = ui.add_sized(
    127             ui.available_size(),
    128             TextEdit::multiline(&mut self.draft.buffer)
    129                 .hint_text(egui::RichText::new("Write a banger note here...").weak())
    130                 .frame(false),
    131         );
    132 
    133         let focused = response.has_focus();
    134 
    135         ui.ctx().data_mut(|d| d.insert_temp(self.id(), focused));
    136 
    137         response
    138     }
    139 
    140     fn focused(&self, ui: &egui::Ui) -> bool {
    141         ui.ctx()
    142             .data(|d| d.get_temp::<bool>(self.id()).unwrap_or(false))
    143     }
    144 
    145     fn id(&self) -> egui::Id {
    146         self.id_source.unwrap_or_else(|| egui::Id::new("post"))
    147     }
    148 
    149     pub fn outer_margin() -> f32 {
    150         16.0
    151     }
    152 
    153     pub fn inner_margin() -> f32 {
    154         12.0
    155     }
    156 
    157     pub fn ui(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> PostResponse {
    158         let focused = self.focused(ui);
    159         let stroke = if focused {
    160             ui.visuals().selection.stroke
    161         } else {
    162             //ui.visuals().selection.stroke
    163             ui.visuals().noninteractive().bg_stroke
    164         };
    165 
    166         let mut frame = egui::Frame::default()
    167             .inner_margin(egui::Margin::same(PostView::inner_margin()))
    168             .outer_margin(egui::Margin::same(PostView::outer_margin()))
    169             .fill(ui.visuals().extreme_bg_color)
    170             .stroke(stroke)
    171             .rounding(12.0);
    172 
    173         if focused {
    174             frame = frame.shadow(egui::epaint::Shadow {
    175                 offset: egui::vec2(0.0, 0.0),
    176                 blur: 8.0,
    177                 spread: 0.0,
    178                 color: stroke.color,
    179             });
    180         }
    181 
    182         frame
    183             .show(ui, |ui| {
    184                 ui.vertical(|ui| {
    185                     let edit_response = ui.horizontal(|ui| self.editbox(txn, ui)).inner;
    186 
    187                     let action = ui
    188                         .horizontal(|ui| {
    189                             if let PostType::Quote(id) = self.post_type {
    190                                 let avail_size = ui.available_size_before_wrap();
    191                                 ui.with_layout(Layout::left_to_right(egui::Align::TOP), |ui| {
    192                                     Frame::none().show(ui, |ui| {
    193                                         ui.vertical(|ui| {
    194                                             ui.set_max_width(avail_size.x * 0.8);
    195                                             render_note_preview(
    196                                                 ui,
    197                                                 self.ndb,
    198                                                 self.note_cache,
    199                                                 self.img_cache,
    200                                                 txn,
    201                                                 id.bytes(),
    202                                                 nostrdb::NoteKey::new(0),
    203                                             );
    204                                         });
    205                                     });
    206                                 });
    207                             }
    208 
    209                             ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| {
    210                                 if ui
    211                                     .add_sized(
    212                                         [91.0, 32.0],
    213                                         post_button(!self.draft.buffer.is_empty()),
    214                                     )
    215                                     .clicked()
    216                                 {
    217                                     let new_post = NewPost::new(
    218                                         self.draft.buffer.clone(),
    219                                         self.poster.to_full(),
    220                                     );
    221                                     Some(PostAction::new(self.post_type.clone(), new_post))
    222                                 } else {
    223                                     None
    224                                 }
    225                             })
    226                             .inner
    227                         })
    228                         .inner;
    229 
    230                     PostResponse {
    231                         action,
    232                         edit_response,
    233                     }
    234                 })
    235                 .inner
    236             })
    237             .inner
    238     }
    239 }
    240 
    241 fn post_button(interactive: bool) -> impl egui::Widget {
    242     move |ui: &mut egui::Ui| {
    243         let button = egui::Button::new("Post now");
    244         if interactive {
    245             ui.add(button)
    246         } else {
    247             ui.add(
    248                 button
    249                     .sense(egui::Sense::hover())
    250                     .fill(ui.visuals().widgets.noninteractive.bg_fill)
    251                     .stroke(ui.visuals().widgets.noninteractive.bg_stroke),
    252             )
    253             .on_hover_cursor(egui::CursorIcon::NotAllowed)
    254         }
    255     }
    256 }
    257 
    258 mod preview {
    259     use super::*;
    260 
    261     pub struct PostPreview {
    262         ndb: Ndb,
    263         img_cache: ImageCache,
    264         note_cache: NoteCache,
    265         draft: Draft,
    266         poster: FullKeypair,
    267     }
    268 
    269     impl PostPreview {
    270         fn new() -> Self {
    271             let ndb = Ndb::new(".", &Config::new()).expect("ndb");
    272 
    273             PostPreview {
    274                 ndb,
    275                 img_cache: ImageCache::new(".".into()),
    276                 note_cache: NoteCache::default(),
    277                 draft: Draft::new(),
    278                 poster: FullKeypair::generate(),
    279             }
    280         }
    281     }
    282 
    283     impl View for PostPreview {
    284         fn ui(&mut self, ui: &mut egui::Ui) {
    285             let txn = Transaction::new(&self.ndb).expect("txn");
    286             PostView::new(
    287                 &self.ndb,
    288                 &mut self.draft,
    289                 PostType::New,
    290                 &mut self.img_cache,
    291                 &mut self.note_cache,
    292                 self.poster.to_filled(),
    293             )
    294             .ui(&txn, ui);
    295         }
    296     }
    297 
    298     impl Preview for PostView<'_> {
    299         type Prev = PostPreview;
    300 
    301         fn preview(_cfg: PreviewConfig) -> Self::Prev {
    302             PostPreview::new()
    303         }
    304     }
    305 }