notedeck

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

commit 91028929b243fb28eaf596a379235825faea6a2b
parent 2eef34fa1c3758c9916aa587255aa0d975df2d06
Author: kernelkind <kernelkind@gmail.com>
Date:   Mon,  8 Sep 2025 15:57:24 -0400

ui: add support for non-notification composite rendering

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

Diffstat:
Mcrates/notedeck_columns/src/ui/timeline.rs | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
1 file changed, 226 insertions(+), 95 deletions(-)

diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs @@ -5,7 +5,7 @@ use enostr::Pubkey; use nostrdb::{Note, ProfileRecord, Transaction}; use notedeck::name::get_display_name; use notedeck::ui::is_narrow; -use notedeck::{tr_plural, JobsCache, Muted}; +use notedeck::{tr_plural, JobsCache, Muted, NotedeckTextStyle}; use notedeck_ui::app_images::{like_image, repost_image}; use notedeck_ui::ProfilePic; use std::f32::consts::PI; @@ -188,6 +188,7 @@ fn timeline_ui( note_context, jobs, ) + .notifications(matches!(timeline_id, TimelineKind::Notifications(_))) .show(ui) }); @@ -375,6 +376,7 @@ fn shrink_range_to_width(range: egui::Rangef, width: f32) -> egui::Rangef { } pub struct TimelineTabView<'a, 'd> { + notifications: bool, tab: &'a TimelineTab, note_options: NoteOptions, txn: &'a Transaction, @@ -392,6 +394,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> { jobs: &'a mut JobsCache, ) -> Self { Self { + notifications: false, tab, note_options, txn, @@ -400,6 +403,11 @@ impl<'a, 'd> TimelineTabView<'a, 'd> { } } + pub fn notifications(mut self, notifications: bool) -> Self { + self.notifications = notifications; + self + } + pub fn show(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { let mut action: Option<NoteAction> = None; let len = self.tab.units.len(); @@ -501,6 +509,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> { self.txn, &underlying_note, repost_unit, + self.notifications, ), }, } @@ -526,6 +535,7 @@ impl CompositeType { first_name: &str, total_count: usize, referenced_type: ReferencedNoteType, + notification: bool, ) -> String { let count = total_count - 1; @@ -533,7 +543,16 @@ impl CompositeType { CompositeType::Reaction => { reaction_description(loc, first_name, count, referenced_type) } - CompositeType::Repost => repost_description(loc, first_name, count, referenced_type), + CompositeType::Repost => repost_description( + loc, + first_name, + count, + if notification { + DescriptionType::Notification(referenced_type) + } else { + DescriptionType::Other + }, + ), } } } @@ -586,46 +605,72 @@ fn reaction_description( } } +enum DescriptionType { + Notification(ReferencedNoteType), + Other, +} + fn repost_description( loc: &mut Localization, first_name: &str, count: usize, - referenced_type: ReferencedNoteType, + description_type: DescriptionType, ) -> String { - match referenced_type { - ReferencedNoteType::Tagged => { - if count == 0 { - tr!( - loc, - "{name} reposted a note you were tagged in", - "repost from user", - name = first_name - ) - } else { - tr_plural!( - loc, - "{name} and {count} other reposted a note you were tagged in", - "{name} and {count} others reposted a note you were tagged in", - "describing the amount of reposts a note you were tagged in received", - count, - name = first_name - ) + match description_type { + DescriptionType::Notification(referenced_type) => match referenced_type { + ReferencedNoteType::Tagged => { + if count == 0 { + tr!( + loc, + "{name} reposted a note you were tagged in", + "repost from user", + name = first_name + ) + } else { + tr_plural!( + loc, + "{name} and {count} other reposted a note you were tagged in", + "{name} and {count} others reposted a note you were tagged in", + "describing the amount of reposts a note you were tagged in received", + count, + name = first_name + ) + } } - } - ReferencedNoteType::Yours => { + ReferencedNoteType::Yours => { + if count == 0 { + tr!( + loc, + "{name} reposted your note", + "repost from user", + name = first_name + ) + } else { + tr_plural!( + loc, + "{name} and {count} other reposted your note", + "{name} and {count} others reposted your note", + "describing the amount of reposts your note received", + count, + name = first_name + ) + } + } + }, + DescriptionType::Other => { if count == 0 { tr!( loc, - "{name} reposted your note", + "{name} reposted", "repost from user", name = first_name ) } else { tr_plural!( loc, - "{name} and {count} other reposted your note", - "{name} and {count} others reposted your note", - "describing the amount of reposts your note received", + "{name} and {count} other reposted", + "{name} and {count} others reposted", + "describing the amount of reposts a note has", count, name = first_name ) @@ -685,9 +730,11 @@ fn render_reaction_cluster( underlying_note, profiles_to_show, CompositeType::Reaction, + true, ) } +#[allow(clippy::too_many_arguments)] fn render_composite_entry( ui: &mut egui::Ui, note_context: &mut NoteContext, @@ -696,6 +743,7 @@ fn render_composite_entry( underlying_note: &nostrdb::Note<'_>, profiles_to_show: Vec<ProfileEntry>, composite_type: CompositeType, + notification: bool, ) -> RenderEntryResponse { let first_name = get_display_name(profiles_to_show.iter().find_map(|opt| opt.record.as_ref())) .name() @@ -703,92 +751,173 @@ fn render_composite_entry( let num_profiles = profiles_to_show.len(); let mut action = None; + + let referenced_type = if note_context + .accounts + .get_selected_account() + .key + .pubkey + .bytes() + != underlying_note.pubkey() + { + ReferencedNoteType::Tagged + } else { + ReferencedNoteType::Yours + }; + 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), - composite_type.image(ui.visuals().dark_mode), - ); - }); - - 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(), - ) - .size(24.0) - .sense(Sense::click()), - ); - - if resp.clicked() { - action = Some(NoteAction::Profile(*entry.pk)) - } - } - }); - }); - }, - ); - - let referenced_type = if note_context - .accounts - .get_selected_account() - .key - .pubkey - .bytes() - != underlying_note.pubkey() - { - ReferencedNoteType::Tagged - } else { - ReferencedNoteType::Yours - }; - - ui.add_space(2.0); - ui.horizontal(|ui| { - ui.add_space(52.0); + let show_label_newline = ui + .horizontal_wrapped(|ui| { + let pfps_resp = ui + .allocate_ui_with_layout( + vec2(ui.available_width(), 32.0), + Layout::left_to_right(egui::Align::Center), + |ui| { + render_profiles( + ui, + profiles_to_show, + &composite_type, + note_context.img_cache, + ) + }, + ) + .inner; + + if let Some(cur_action) = pfps_resp.action { + action = Some(cur_action); + } - ui.horizontal_wrapped(|ui| { - ui.label(composite_type.description( + let description = composite_type.description( note_context.i18n, &first_name, num_profiles, referenced_type, - )) + notification, + ); + let galley = ui.painter().layout_no_wrap( + description.clone(), + NotedeckTextStyle::Body.get_font_id(ui.ctx()), + ui.visuals().text_color(), + ); + + ui.add_space(4.0); + + let galley_pos = { + let mut galley_pos = ui.next_widget_position(); + galley_pos.y = pfps_resp.resp.rect.right_center().y; + galley_pos.y -= galley.rect.height() / 2.0; + galley_pos + }; + + let fits_no_wrap = { + let mut rightmost_pos = galley_pos; + rightmost_pos.x += galley.rect.width(); + + ui.available_rect_before_wrap().contains(rightmost_pos) + }; + + if fits_no_wrap { + ui.painter() + .galley(galley_pos, galley, ui.visuals().text_color()); + None + } else { + Some(description) + } + }) + .inner; + + if let Some(desc) = show_label_newline { + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.add_space(48.0); + ui.horizontal_wrapped(|ui| { + ui.label(desc); + }); }); - }); + } ui.add_space(16.0); - 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, underlying_note, options, jobs).show(ui); - - if let Some(note_action) = resp.action { - action = Some(note_action); - } - }); + let resp = ui + .horizontal(|ui| { + let mut options = note_options; + if notification { + options = options + .difference(NoteOptions::ActionBar | NoteOptions::OptionsButton) + .union(NoteOptions::NotificationPreview); + + ui.add_space(48.0); + }; + NoteView::new(note_context, underlying_note, options, jobs).show(ui) + }) + .inner; + + if let Some(note_action) = resp.action { + action.get_or_insert(note_action); + } }); notedeck_ui::hline(ui); RenderEntryResponse::Success(action) } +fn render_profiles( + ui: &mut egui::Ui, + profiles_to_show: Vec<ProfileEntry>, + composite_type: &CompositeType, + img_cache: &mut notedeck::Images, +) -> PfpsResponse { + let mut action = None; + ui.vertical(|ui| { + ui.add_space(4.0); + ui.add_sized( + vec2(28.0, 28.0), + composite_type.image(ui.visuals().dark_mode), + ); + }); + + ui.add_space(16.0); + + let resp = ui.horizontal(|ui| { + ScrollArea::horizontal() + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .show(ui, |ui| { + let mut last_pfp_resp = None; + for entry in profiles_to_show { + let resp = ui.add( + &mut ProfilePic::from_profile_or_default(img_cache, entry.record.as_ref()) + .size(24.0) + .sense(Sense::click()), + ); + + last_pfp_resp = Some(resp.clone()); + + if resp.clicked() { + action = Some(NoteAction::Profile(*entry.pk)) + } + } + + last_pfp_resp + }) + .inner + }); + + let resp = if let Some(r) = resp.inner { + r + } else { + resp.response + }; + + PfpsResponse { action, resp } +} + +struct PfpsResponse { + action: Option<NoteAction>, + resp: egui::Response, +} + #[allow(clippy::too_many_arguments)] fn render_repost_cluster( ui: &mut egui::Ui, @@ -799,6 +928,7 @@ fn render_repost_cluster( txn: &Transaction, underlying_note: &Note, repost: &RepostUnit, + notifications: bool, ) -> RenderEntryResponse { let profiles_to_show: Vec<ProfileEntry> = repost .reposts @@ -818,6 +948,7 @@ fn render_repost_cluster( underlying_note, profiles_to_show, CompositeType::Repost, + notifications, ) }