notedeck

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

commit a6856867a992e3ecbffb8d1224405a0382e2cb31
parent 8ef6534981a11faff8448c348e9a3873e0f754a5
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 25 Jun 2024 13:20:56 -0500

Merge remote-tracking branch 'pr/107'

Diffstat:
Msrc/app.rs | 17++++++-----------
Msrc/colors.rs | 8++++----
Msrc/timeline.rs | 6+-----
Msrc/ui/account_login_view.rs | 4++--
Msrc/ui/mod.rs | 2+-
Msrc/ui/note/mod.rs | 18+++++++++++++++---
Msrc/ui/note/options.rs | 18+++++++++++++++++-
Msrc/ui/note/post.rs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/ui/note/reply.rs | 107++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/ui/profile/picture.rs | 13+++++++++++++
10 files changed, 231 insertions(+), 56 deletions(-)

diff --git a/src/app.rs b/src/app.rs @@ -930,17 +930,12 @@ fn render_nav(routes: Vec<Route>, timeline_ind: usize, app: &mut Damus, ui: &mut return; }; - let note_key = note.key().unwrap(); - - let poster = app - .account_manager - .get_selected_account_index() - .unwrap_or(0); - - let replying_to = note.pubkey(); - ui::PostView::new(&mut app, poster, replying_to) - .id_source(("post", timeline_ind, note_key)) - .ui(&txn, ui); + let id = egui::Id::new(("post", timeline_ind, note.key().unwrap())); + egui::ScrollArea::vertical().show(ui, |ui| { + ui::PostReplyView::new(&mut app, &note) + .id_source(id) + .show(ui); + }); } }); diff --git a/src/colors.rs b/src/colors.rs @@ -8,7 +8,7 @@ pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9); pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A); const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00); const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A); -const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A); +//const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A); // BACKGROUNDS const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39); @@ -29,7 +29,7 @@ pub struct ColorTheme { pub extreme_bg_color: Color32, pub text_color: Color32, pub err_fg_color: Color32, - pub warn_fg_color: Color32, + //pub warn_fg_color: Color32, pub hyperlink_color: Color32, pub selection_color: Color32, @@ -56,7 +56,7 @@ pub fn desktop_dark_color_theme() -> ColorTheme { extreme_bg_color: DARK_ISH_BG, text_color: Color32::WHITE, err_fg_color: RED_700, - warn_fg_color: ORANGE_700, + //warn_fg_color: ORANGE_700, hyperlink_color: PURPLE, selection_color: PURPLE_ALT, @@ -92,7 +92,7 @@ pub fn light_color_theme() -> ColorTheme { extreme_bg_color: LIGHTER_GRAY, text_color: BLACK, err_fg_color: RED_700, - warn_fg_color: ORANGE_700, + //warn_fg_color: ORANGE_700, hyperlink_color: PURPLE, selection_color: PURPLE_ALT, diff --git a/src/timeline.rs b/src/timeline.rs @@ -225,11 +225,7 @@ fn tabs_ui(timeline: &mut Timeline, ui: &mut egui::Ui) { //ui.add_space(0.5); ui::hline(ui); - let sel = if let Some(sel) = tab_res.selected() { - sel - } else { - 0 - }; + let sel = tab_res.selected().unwrap_or_default(); // fun animation timeline.selected_view = sel; diff --git a/src/ui/account_login_view.rs b/src/ui/account_login_view.rs @@ -30,7 +30,7 @@ impl<'a> View for AccountLoginView<'a> { if self.is_mobile { self.show_mobile(ui); } else { - self.show(ui); + self.ui(ui); } } } @@ -44,7 +44,7 @@ impl<'a> AccountLoginView<'a> { } } - fn show(&mut self, ui: &mut egui::Ui) -> egui::Response { + fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { let screen_width = ui.ctx().screen_rect().max.x; let screen_height = ui.ctx().screen_rect().max.y; diff --git a/src/ui/mod.rs b/src/ui/mod.rs @@ -17,7 +17,7 @@ pub use account_switcher::AccountSelectionWidget; pub use fixed_window::{FixedWindow, FixedWindowResponse}; pub use global_popup::DesktopGlobalPopup; pub use mention::Mention; -pub use note::{BarAction, Note, NoteResponse, PostView}; +pub use note::{BarAction, Note, NoteResponse, PostReplyView, PostView}; pub use preview::{Preview, PreviewApp, PreviewConfig}; pub use profile::{profile_preview_controller, ProfilePic, ProfilePreview}; pub use relay::RelayView; diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs @@ -1,10 +1,12 @@ pub mod contents; pub mod options; pub mod post; +pub mod reply; pub use contents::NoteContents; pub use options::NoteOptions; pub use post::PostView; +pub use reply::PostReplyView; use crate::{colors, notecache::CachedNote, ui, ui::View, Damus}; use egui::{Label, RichText, Sense}; @@ -128,6 +130,11 @@ impl<'a> Note<'a> { self } + pub fn medium_pfp(mut self, enable: bool) -> Self { + self.options_mut().set_medium_pfp(enable); + self + } + pub fn note_previews(mut self, enable: bool) -> Self { self.options_mut().set_note_previews(enable); self @@ -179,6 +186,10 @@ impl<'a> Note<'a> { .response } + pub fn expand_size() -> f32 { + 5.0 + } + fn pfp( &mut self, note_key: NoteKey, @@ -188,7 +199,9 @@ impl<'a> Note<'a> { ui.spacing_mut().item_spacing.x = 16.0; let pfp_size = if self.options().has_small_pfp() { - 24.0 + ui::ProfilePic::small_size() + } else if self.options().has_medium_pfp() { + ui::ProfilePic::medium_size() } else { ui::ProfilePic::default_size() }; @@ -201,7 +214,6 @@ impl<'a> Note<'a> { // these have different lifetimes and types, // so the calls must be separate Some(pic) => { - let expand_size = 5.0; let anim_speed = 0.05; let profile_key = profile.as_ref().unwrap().record().note_key(); let note_key = note_key.as_u64(); @@ -213,7 +225,7 @@ impl<'a> Note<'a> { ui, egui::Id::new((profile_key, note_key)), pfp_size, - expand_size, + ui::Note::expand_size(), anim_speed, ); diff --git a/src/ui/note/options.rs b/src/ui/note/options.rs @@ -8,7 +8,8 @@ bitflags! { const actionbar = 0b00000001; const note_previews = 0b00000010; const small_pfp = 0b00000100; - const wide = 0b00001000; + const medium_pfp = 0b00001000; + const wide = 0b00010000; } } @@ -29,6 +30,11 @@ impl NoteOptions { } #[inline] + pub fn has_medium_pfp(self) -> bool { + (self & NoteOptions::medium_pfp) == NoteOptions::medium_pfp + } + + #[inline] pub fn has_wide(self) -> bool { (self & NoteOptions::wide) == NoteOptions::wide } @@ -41,6 +47,16 @@ impl NoteOptions { *self &= !NoteOptions::small_pfp; } } + + #[inline] + pub fn set_medium_pfp(&mut self, enable: bool) { + if enable { + *self |= NoteOptions::medium_pfp; + } else { + *self &= !NoteOptions::medium_pfp; + } + } + #[inline] pub fn set_note_previews(&mut self, enable: bool) { if enable { diff --git a/src/ui/note/post.rs b/src/ui/note/post.rs @@ -1,9 +1,9 @@ use crate::app::Damus; +use crate::draft::Draft; use crate::ui; use crate::ui::{Preview, PreviewConfig, View}; use egui::widgets::text_edit::TextEdit; use nostrdb::Transaction; -use tracing::info; pub struct PostView<'app, 'p> { app: &'app mut Damus, @@ -13,6 +13,20 @@ pub struct PostView<'app, 'p> { replying_to: &'p [u8; 32], } +pub struct NewPost { + pub content: String, + pub account: usize, +} + +pub enum PostAction { + Post(NewPost), +} + +pub struct PostResponse { + pub action: Option<PostAction>, + pub edit_response: egui::Response, +} + impl<'app, 'p> PostView<'app, 'p> { pub fn new(app: &'app mut Damus, poster: usize, replying_to: &'p [u8; 32]) -> Self { let id_source: Option<egui::Id> = None; @@ -29,7 +43,14 @@ impl<'app, 'p> PostView<'app, 'p> { self } - fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) { + fn draft(&mut self) -> &mut Draft { + self.app + .drafts + .entry(enostr::NoteId::new(*self.replying_to)) + .or_default() + } + + fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response { ui.spacing_mut().item_spacing.x = 12.0; let pfp_size = 24.0; @@ -61,17 +82,13 @@ impl<'app, 'p> PostView<'app, 'p> { ); } - let draft = self - .app - .drafts - .entry(enostr::NoteId::new(*self.replying_to)) - .or_default(); + let response = ui.add(TextEdit::multiline(&mut self.draft().buffer).frame(false)); - let focused = ui - .add(TextEdit::multiline(&mut draft.buffer).frame(false)) - .has_focus(); + let focused = response.has_focus(); ui.ctx().data_mut(|d| d.insert_temp(self.id(), focused)); + + response } fn focused(&self, ui: &egui::Ui) -> bool { @@ -83,7 +100,15 @@ impl<'app, 'p> PostView<'app, 'p> { self.id_source.unwrap_or_else(|| egui::Id::new("post")) } - pub fn ui(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) { + pub fn outer_margin() -> f32 { + 16.0 + } + + pub fn inner_margin() -> f32 { + 12.0 + } + + pub fn ui(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> PostResponse { let focused = self.focused(ui); let stroke = if focused { ui.visuals().selection.stroke @@ -93,8 +118,8 @@ impl<'app, 'p> PostView<'app, 'p> { }; let mut frame = egui::Frame::default() - .inner_margin(egui::Margin::same(12.0)) - .outer_margin(egui::Margin::same(12.0)) + .inner_margin(egui::Margin::same(PostView::inner_margin())) + .outer_margin(egui::Margin::same(PostView::outer_margin())) .fill(ui.visuals().extreme_bg_color) .stroke(stroke) .rounding(12.0); @@ -108,22 +133,35 @@ impl<'app, 'p> PostView<'app, 'p> { }); } - frame.show(ui, |ui| { - ui.vertical(|ui| { - ui.horizontal(|ui| { - self.editbox(txn, ui); - }); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - if ui - .add_sized([91.0, 32.0], egui::Button::new("Post now")) - .clicked() - { - info!("Post clicked"); + frame + .show(ui, |ui| { + ui.vertical(|ui| { + let edit_response = ui.horizontal(|ui| self.editbox(txn, ui)).inner; + + let action = ui + .with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + if ui + .add_sized([91.0, 32.0], egui::Button::new("Post now")) + .clicked() + { + Some(PostAction::Post(NewPost { + content: self.draft().buffer.clone(), + account: self.poster, + })) + } else { + None + } + }) + .inner; + + PostResponse { + action, + edit_response, } - }); - }); - }); + }) + .inner + }) + .inner } } diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs @@ -1 +1,106 @@ -struct PostReplyView {} +use crate::{ui, Damus}; + +pub struct PostReplyView<'a> { + app: &'a mut Damus, + id_source: Option<egui::Id>, + note: &'a nostrdb::Note<'a>, +} + +impl<'a> PostReplyView<'a> { + pub fn new(app: &'a mut Damus, note: &'a nostrdb::Note<'a>) -> Self { + let id_source: Option<egui::Id> = None; + PostReplyView { + app, + id_source, + note, + } + } + + pub fn id_source(mut self, id: egui::Id) -> Self { + self.id_source = Some(id); + self + } + + pub fn id(&self) -> egui::Id { + self.id_source + .unwrap_or_else(|| egui::Id::new("post-reply-view")) + } + + pub fn show(&mut self, ui: &mut egui::Ui) { + ui.vertical(|ui| { + let avail_rect = ui.available_rect_before_wrap(); + + // This is the offset of the post view's pfp. We use this + // to indent things so that the reply line is aligned + let pfp_offset = ui::PostView::outer_margin() + + ui::PostView::inner_margin() + + ui::ProfilePic::small_size() / 2.0; + + let note_offset = + pfp_offset - ui::ProfilePic::medium_size() / 2.0 - ui::Note::expand_size() / 2.0; + + egui::Frame::none() + .outer_margin(egui::Margin::same(note_offset)) + .show(ui, |ui| { + ui::Note::new(self.app, self.note) + .actionbar(false) + .medium_pfp(true) + .show(ui); + }); + + let poster = self + .app + .account_manager + .get_selected_account_index() + .unwrap_or(0); + + let replying_to = self.note.pubkey(); + let rect_before_post = ui.min_rect(); + + let id = self.id(); + let post_response = ui::PostView::new(self.app, poster, replying_to) + .id_source(id) + .ui(self.note.txn().unwrap(), ui); + + // + // reply line + // + + // Position and draw the reply line + let mut rect = ui.min_rect(); + + // Position the line right above the poster's profile pic in + // the post box. Use the PostView's margin values to + // determine this offset. + rect.min.x = avail_rect.min.x + pfp_offset; + + // honestly don't know what the fuck I'm doing here. just trying + // to get the line under the profile picture + rect.min.y = avail_rect.min.y + + (ui::ProfilePic::medium_size() / 2.0 + + ui::ProfilePic::medium_size() + + ui::Note::expand_size() * 2.0) + + 1.0; + + // For some reason we need to nudge the reply line's height a + // few more pixels? + let nudge = if post_response.edit_response.has_focus() { + // we nudge by one less pixel if focused, otherwise it + // overlaps the focused PostView purple border color + 2.0 + } else { + // we have to nudge by one more pixel when not focused + // otherwise it looks like there's a gap(?) + 3.0 + }; + + rect.max.y = rect_before_post.max.y + ui::PostView::outer_margin() + nudge; + + ui.painter().vline( + rect.left(), + rect.y_range(), + ui.visuals().widgets.noninteractive.bg_stroke, + ); + }); + } +} diff --git a/src/ui/profile/picture.rs b/src/ui/profile/picture.rs @@ -31,14 +31,27 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> { .map(|url| ProfilePic::new(cache, url)) } + #[inline] pub fn default_size() -> f32 { 38.0 } + #[inline] + pub fn medium_size() -> f32 { + 32.0 + } + + #[inline] + pub fn small_size() -> f32 { + 24.0 + } + + #[inline] pub fn no_pfp_url() -> &'static str { "https://damus.io/img/no-profile.svg" } + #[inline] pub fn size(mut self, size: f32) -> Self { self.size = size; self