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, "ing) 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 }