ui: reactions closer approximation of iOS design

Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
kernelkind
2025-08-25 20:00:17 -04:00
parent 30af03cfcc
commit 529377a706
4 changed files with 138 additions and 111 deletions

View File

@@ -1,5 +1,5 @@
use egui::containers::scroll_area::ScrollBarVisibility; 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 egui_tabs::TabColor;
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{ProfileRecord, Transaction}; use nostrdb::{ProfileRecord, Transaction};
@@ -7,7 +7,7 @@ use notedeck::name::get_display_name;
use notedeck::ui::is_narrow; use notedeck::ui::is_narrow;
use notedeck::{JobsCache, Muted, NoteRef}; use notedeck::{JobsCache, Muted, NoteRef};
use notedeck_ui::app_images::like_image; use notedeck_ui::app_images::like_image;
use notedeck_ui::{padding, ProfilePic}; use notedeck_ui::ProfilePic;
use std::f32::consts::PI; use std::f32::consts::PI;
use tracing::{error, warn}; use tracing::{error, warn};
@@ -540,80 +540,86 @@ fn render_reaction_cluster(
let num_profiles_other = profiles_to_show.len() - 1; let num_profiles_other = profiles_to_show.len() - 1;
let mut action = None; let mut action = None;
padding(8.0, ui, |ui| { egui::Frame::new()
ui.allocate_ui_with_layout( .inner_margin(Margin::symmetric(8, 4))
vec2(ui.available_width(), 32.0), .show(ui, |ui| {
Layout::left_to_right(egui::Align::Center), ui.allocate_ui_with_layout(
|ui| { vec2(ui.available_width(), 32.0),
ui.vertical(|ui| { 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_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(),
)
.size(24.0)
.sense(Sense::click()),
);
ui.horizontal(|ui| { if resp.clicked() {
ScrollArea::horizontal() action = Some(NoteAction::Profile(*entry.pk))
.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))
} }
} });
}); });
}); },
}, );
);
let note_type_desc = if note_context let note_type_desc = if note_context
.accounts .accounts
.get_selected_account() .get_selected_account()
.key .key
.pubkey .pubkey
.bytes() .bytes()
!= reacted_to_note.pubkey() != reacted_to_note.pubkey()
{ {
"note you were tagged in" "note you were tagged in"
} else { } else {
"your note" "your note"
}; };
ui.add_space(2.0); ui.add_space(2.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(52.0); ui.add_space(52.0);
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
if num_profiles_other > 0 { if num_profiles_other > 0 {
ui.label(format!( ui.label(format!(
"{first_name} and {num_profiles_other} others reacted to {note_type_desc}", "{first_name} and {num_profiles_other} others reacted to {note_type_desc}",
)); ));
} else { } else {
ui.label(format!("{first_name} reacted to {note_type_desc}")); ui.label(format!("{first_name} reacted to {note_type_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, &reacted_to_note, options, jobs).show(ui);
if let Some(note_action) = resp.action {
action = Some(note_action);
} }
}); });
}); });
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);
if let Some(note_action) = resp.action {
action = Some(note_action);
}
});
});
notedeck_ui::hline(ui); notedeck_ui::hline(ui);
RenderEntryResponse::Success(action) RenderEntryResponse::Success(action)
} }

View File

@@ -334,14 +334,14 @@ fn render_undecorated_note_contents<'a>(
.selectable(selectable), .selectable(selectable),
); );
} else { } else {
ui.add( let mut richtext = RichText::new(block_str)
Label::new( .text_style(NotedeckTextStyle::NoteBody.text_style());
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));
.wrap() }
.selectable(selectable),
); ui.add(Label::new(richtext).wrap().selectable(selectable));
} }
// don't render any more blocks // don't render any more blocks
if truncate { if truncate {

View File

@@ -426,16 +426,19 @@ impl<'a, 'd> NoteView<'a, 'd> {
) -> egui::InnerResponse<NoteUiResponse> { ) -> egui::InnerResponse<NoteUiResponse> {
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
let mut note_action: Option<NoteAction> = None; let mut note_action: Option<NoteAction> = None;
let pfp_rect = ui let mut pfp_rect = None;
.horizontal(|ui| {
if !self.flags.contains(NoteOptions::NotificationPreview) {
ui.horizontal(|ui| {
let pfp_resp = self.pfp(note_key, profile, 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 note_action = pfp_resp
.into_action(self.note.pubkey()) .into_action(self.note.pubkey())
.or(note_action.take()); .or(note_action.take());
let size = ui.available_size(); let size = ui.available_size();
ui.vertical(|ui| 's: {
ui.vertical(|ui| {
ui.add_sized( ui.add_sized(
[size.x, self.options().pfp_size() as f32], [size.x, self.options().pfp_size() as f32],
|ui: &mut egui::Ui| { |ui: &mut egui::Ui| {
@@ -460,7 +463,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
.borrow(self.note.tags()); .borrow(self.note.tags());
if note_reply.reply().is_none() { if note_reply.reply().is_none() {
break 's; return;
} }
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
@@ -477,10 +480,8 @@ impl<'a, 'd> NoteView<'a, 'd> {
.or(note_action.take()); .or(note_action.take());
}); });
}); });
});
pfp_rect }
})
.inner;
let mut contents = let mut contents =
NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs); 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> { ) -> egui::InnerResponse<NoteUiResponse> {
// main design // main design
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
let pfp_resp = self.pfp(note_key, profile, ui); let (mut note_action, pfp_rect) =
let pfp_rect = pfp_resp.bounding_rect; if self.flags.contains(NoteOptions::NotificationPreview) {
let mut note_action: Option<NoteAction> = pfp_resp.into_action(self.note.pubkey()); // 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| { 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.horizontal_wrapped(|ui| 's: {
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());
if note_reply.reply().is_none() {
break 's;
}
note_action = reply_desc(
ui, ui,
txn, self.note_context.i18n,
&note_reply, self.note,
self.note_context, profile,
self.flags, self.flags,
self.jobs, );
)
.or(note_action.take()); 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());
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());
});
}
let mut contents = let mut contents =
NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs); 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()))) .then_some(NoteAction::note(NoteId::new(*self.note.id())))
.or(note_action); .or(note_action);
NoteResponse::new(response.response) let mut resp = NoteResponse::new(response.response).with_action(note_action);
.with_action(note_action) if let Some(pfp_rect) = note_ui_resp.pfp_rect {
.with_pfp(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 { struct NoteUiResponse {
action: Option<NoteAction>, action: Option<NoteAction>,
pfp_rect: egui::Rect, pfp_rect: Option<egui::Rect>,
} }
struct PfpResponse { struct PfpResponse {

View File

@@ -38,6 +38,9 @@ bitflags! {
/// no animation override (accessibility) /// no animation override (accessibility)
const NoAnimations = 1 << 17; const NoAnimations = 1 << 17;
/// Styled for a notification preview
const NotificationPreview = 1 << 18;
} }
} }