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