notedeck

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

commit 529377a706afefe142735311c9242e4d7259577c
parent 30af03cfccc25889c7bba41b9db3ca2bed71ccbd
Author: kernelkind <kernelkind@gmail.com>
Date:   Mon, 25 Aug 2025 20:00:17 -0400

ui: reactions closer approximation of iOS design

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck_columns/src/ui/timeline.rs | 132+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mcrates/notedeck_ui/src/note/contents.rs | 16++++++++--------
Mcrates/notedeck_ui/src/note/mod.rs | 94+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/notedeck_ui/src/note/options.rs | 3+++
4 files changed, 136 insertions(+), 109 deletions(-)

diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs @@ -1,5 +1,5 @@ use egui::containers::scroll_area::ScrollBarVisibility; -use egui::{vec2, Direction, Layout, Pos2, ScrollArea, Sense, Stroke}; +use egui::{vec2, Direction, Layout, Margin, Pos2, ScrollArea, Sense, Stroke}; use egui_tabs::TabColor; use enostr::Pubkey; use nostrdb::{ProfileRecord, Transaction}; @@ -7,7 +7,7 @@ use notedeck::name::get_display_name; use notedeck::ui::is_narrow; use notedeck::{JobsCache, Muted, NoteRef}; use notedeck_ui::app_images::like_image; -use notedeck_ui::{padding, ProfilePic}; +use notedeck_ui::ProfilePic; use std::f32::consts::PI; use tracing::{error, warn}; @@ -540,79 +540,85 @@ fn render_reaction_cluster( let num_profiles_other = profiles_to_show.len() - 1; let mut action = None; - padding(8.0, ui, |ui| { - ui.allocate_ui_with_layout( - vec2(ui.available_width(), 32.0), - Layout::left_to_right(egui::Align::Center), - |ui| { - ui.vertical(|ui| { + egui::Frame::new() + .inner_margin(Margin::symmetric(8, 4)) + .show(ui, |ui| { + ui.allocate_ui_with_layout( + vec2(ui.available_width(), 32.0), + Layout::left_to_right(egui::Align::Center), + |ui| { + ui.vertical(|ui| { + ui.add_space(4.0); + ui.add_sized(vec2(28.0, 28.0), like_image()); + }); + ui.add_space(16.0); - ui.add_sized(vec2(32.0, 32.0), like_image()); - }); - ui.add_space(16.0); - - ui.horizontal(|ui| { - ScrollArea::horizontal() - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .show(ui, |ui| { - for entry in profiles_to_show { - let resp = ui.add( - &mut ProfilePic::from_profile_or_default( - note_context.img_cache, - entry.record.as_ref(), - ) - .sense(Sense::click()), - ); - - if resp.clicked() { - action = Some(NoteAction::Profile(*entry.pk)) + ui.horizontal(|ui| { + ScrollArea::horizontal() + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .show(ui, |ui| { + for entry in profiles_to_show { + let resp = ui.add( + &mut ProfilePic::from_profile_or_default( + note_context.img_cache, + entry.record.as_ref(), + ) + .size(24.0) + .sense(Sense::click()), + ); + + if resp.clicked() { + action = Some(NoteAction::Profile(*entry.pk)) + } } - } - }); - }); - }, - ); - - let note_type_desc = if note_context - .accounts - .get_selected_account() - .key - .pubkey - .bytes() - != reacted_to_note.pubkey() - { - "note you were tagged in" - } else { - "your note" - }; + }); + }); + }, + ); + + let note_type_desc = if note_context + .accounts + .get_selected_account() + .key + .pubkey + .bytes() + != reacted_to_note.pubkey() + { + "note you were tagged in" + } else { + "your note" + }; - ui.add_space(2.0); - ui.horizontal(|ui| { - ui.add_space(52.0); + ui.add_space(2.0); + ui.horizontal(|ui| { + ui.add_space(52.0); - ui.horizontal_wrapped(|ui| { - if num_profiles_other > 0 { - ui.label(format!( + ui.horizontal_wrapped(|ui| { + if num_profiles_other > 0 { + ui.label(format!( "{first_name} and {num_profiles_other} others reacted to {note_type_desc}", )); - } else { - ui.label(format!("{first_name} reacted to {note_type_desc}")); - } + } else { + ui.label(format!("{first_name} reacted to {note_type_desc}")); + } + }); }); - }); - ui.add_space(16.0); + ui.add_space(16.0); - ui.horizontal(|ui| { - ui.add_space(48.0); - let resp = NoteView::new(note_context, &reacted_to_note, note_options, jobs).show(ui); + ui.horizontal(|ui| { + ui.add_space(48.0); + let options = note_options + .difference(NoteOptions::ActionBar | NoteOptions::OptionsButton) + .union(NoteOptions::NotificationPreview); + let resp = NoteView::new(note_context, &reacted_to_note, options, jobs).show(ui); - if let Some(note_action) = resp.action { - action = Some(note_action); - } + if let Some(note_action) = resp.action { + action = Some(note_action); + } + }); }); - }); notedeck_ui::hline(ui); RenderEntryResponse::Success(action) diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs @@ -334,14 +334,14 @@ fn render_undecorated_note_contents<'a>( .selectable(selectable), ); } else { - ui.add( - Label::new( - RichText::new(block_str) - .text_style(NotedeckTextStyle::NoteBody.text_style()), - ) - .wrap() - .selectable(selectable), - ); + let mut richtext = RichText::new(block_str) + .text_style(NotedeckTextStyle::NoteBody.text_style()); + + if options.contains(NoteOptions::NotificationPreview) { + richtext = richtext.color(egui::Color32::from_rgb(0x87, 0x87, 0x8D)); + } + + ui.add(Label::new(richtext).wrap().selectable(selectable)); } // don't render any more blocks if truncate { diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs @@ -426,16 +426,19 @@ impl<'a, 'd> NoteView<'a, 'd> { ) -> egui::InnerResponse<NoteUiResponse> { ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { let mut note_action: Option<NoteAction> = None; - let pfp_rect = ui - .horizontal(|ui| { + let mut pfp_rect = None; + + if !self.flags.contains(NoteOptions::NotificationPreview) { + ui.horizontal(|ui| { let pfp_resp = self.pfp(note_key, profile, ui); - let pfp_rect = pfp_resp.bounding_rect; + pfp_rect = Some(pfp_resp.bounding_rect); note_action = pfp_resp .into_action(self.note.pubkey()) .or(note_action.take()); let size = ui.available_size(); - ui.vertical(|ui| 's: { + + ui.vertical(|ui| { ui.add_sized( [size.x, self.options().pfp_size() as f32], |ui: &mut egui::Ui| { @@ -460,7 +463,7 @@ impl<'a, 'd> NoteView<'a, 'd> { .borrow(self.note.tags()); if note_reply.reply().is_none() { - break 's; + return; } ui.horizontal_wrapped(|ui| { @@ -477,10 +480,8 @@ impl<'a, 'd> NoteView<'a, 'd> { .or(note_action.take()); }); }); - - pfp_rect - }) - .inner; + }); + } let mut contents = NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs); @@ -530,37 +531,51 @@ impl<'a, 'd> NoteView<'a, 'd> { ) -> egui::InnerResponse<NoteUiResponse> { // main design ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { - let pfp_resp = self.pfp(note_key, profile, ui); - let pfp_rect = pfp_resp.bounding_rect; - let mut note_action: Option<NoteAction> = pfp_resp.into_action(self.note.pubkey()); + let (mut note_action, pfp_rect) = + if self.flags.contains(NoteOptions::NotificationPreview) { + // do not render pfp + (None, None) + } else { + let pfp_resp = self.pfp(note_key, profile, ui); + let pfp_rect = pfp_resp.bounding_rect; + (pfp_resp.into_action(self.note.pubkey()), Some(pfp_rect)) + }; ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - NoteView::note_header(ui, self.note_context.i18n, self.note, profile, self.flags); + if !self.flags.contains(NoteOptions::NotificationPreview) { + NoteView::note_header( + ui, + self.note_context.i18n, + self.note, + profile, + self.flags, + ); - ui.horizontal_wrapped(|ui| 's: { - ui.spacing_mut().item_spacing.x = 1.0; + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 1.0; - let note_reply = self - .note_context - .note_cache - .cached_note_or_insert_mut(note_key, self.note) - .reply - .borrow(self.note.tags()); + let note_reply = self + .note_context + .note_cache + .cached_note_or_insert_mut(note_key, self.note) + .reply + .borrow(self.note.tags()); - if note_reply.reply().is_none() { - break 's; - } + if note_reply.reply().is_none() { + return; + } - note_action = reply_desc( - ui, - txn, - &note_reply, - self.note_context, - self.flags, - self.jobs, - ) - .or(note_action.take()); - }); + note_action = reply_desc( + ui, + txn, + &note_reply, + self.note_context, + self.flags, + self.jobs, + ) + .or(note_action.take()); + }); + } let mut contents = NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs); @@ -639,9 +654,12 @@ impl<'a, 'd> NoteView<'a, 'd> { .then_some(NoteAction::note(NoteId::new(*self.note.id()))) .or(note_action); - NoteResponse::new(response.response) - .with_action(note_action) - .with_pfp(note_ui_resp.pfp_rect) + let mut resp = NoteResponse::new(response.response).with_action(note_action); + if let Some(pfp_rect) = note_ui_resp.pfp_rect { + resp = resp.with_pfp(pfp_rect); + } + + resp } } @@ -687,7 +705,7 @@ fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option struct NoteUiResponse { action: Option<NoteAction>, - pfp_rect: egui::Rect, + pfp_rect: Option<egui::Rect>, } struct PfpResponse { diff --git a/crates/notedeck_ui/src/note/options.rs b/crates/notedeck_ui/src/note/options.rs @@ -38,6 +38,9 @@ bitflags! { /// no animation override (accessibility) const NoAnimations = 1 << 17; + + /// Styled for a notification preview + const NotificationPreview = 1 << 18; } }