commit 7d4e9799e531f5cec273cc8b4dece2682ec035b8
parent 55d7cd3379fe8aaa9ba4c1965df45f468171fbbe
Author: kernelkind <kernelkind@gmail.com>
Date: Mon, 25 Aug 2025 10:05:28 -0400
ui: add rendering for `NoteUnit`s
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
1 file changed, 204 insertions(+), 4 deletions(-)
diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs
@@ -1,13 +1,19 @@
use egui::containers::scroll_area::ScrollBarVisibility;
-use egui::{vec2, Direction, Layout, Pos2, Stroke};
+use egui::{vec2, Direction, Layout, Pos2, ScrollArea, Sense, Stroke};
use egui_tabs::TabColor;
-use nostrdb::Transaction;
+use enostr::Pubkey;
+use nostrdb::{ProfileRecord, Transaction};
+use notedeck::name::get_display_name;
use notedeck::ui::is_narrow;
-use notedeck::JobsCache;
+use notedeck::{JobsCache, Muted, NoteRef};
+use notedeck_ui::app_images::like_image;
+use notedeck_ui::{padding, ProfilePic};
use std::f32::consts::PI;
use tracing::{error, warn};
-use crate::timeline::{TimelineCache, TimelineKind, TimelineTab, ViewFilter};
+use crate::timeline::{
+ CompositeUnit, NoteUnit, ReactionUnit, TimelineCache, TimelineKind, TimelineTab, ViewFilter,
+};
use notedeck::{
note::root_note_id_from_selected_id, tr, Localization, NoteAction, NoteContext, ScrollInfo,
};
@@ -467,4 +473,198 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
action
}
+
+ fn render_entry(
+ &mut self,
+ ui: &mut egui::Ui,
+ entry: &NoteUnit,
+ mute: &std::sync::Arc<Muted>,
+ ) -> RenderEntryResponse {
+ match entry {
+ NoteUnit::Single(note_ref) => render_note(
+ ui,
+ self.note_context,
+ self.note_options,
+ self.jobs,
+ mute,
+ self.txn,
+ note_ref,
+ ),
+ NoteUnit::Composite(composite) => match composite {
+ CompositeUnit::Reaction(reaction_unit) => render_reaction_cluster(
+ ui,
+ self.note_context,
+ self.note_options,
+ self.jobs,
+ mute,
+ self.txn,
+ reaction_unit,
+ ),
+ },
+ }
+ }
+}
+
+fn render_note(
+ ui: &mut egui::Ui,
+ note_context: &mut NoteContext,
+ note_options: NoteOptions,
+ jobs: &mut JobsCache,
+ mute: &std::sync::Arc<Muted>,
+ txn: &Transaction,
+ note_ref: &NoteRef,
+) -> RenderEntryResponse {
+ let note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, note_ref.key) {
+ note
+ } else {
+ warn!("failed to query note {:?}", note_ref.key);
+ return RenderEntryResponse::Unsuccessful;
+ };
+
+ let muted = if let Ok(root_id) =
+ root_note_id_from_selected_id(note_context.ndb, note_context.note_cache, txn, note.id())
+ {
+ mute.is_muted(¬e, root_id.bytes())
+ } else {
+ false
+ };
+
+ if muted {
+ return RenderEntryResponse::Success(None);
+ }
+
+ let mut action = None;
+ notedeck_ui::padding(8.0, ui, |ui| {
+ let resp = NoteView::new(note_context, ¬e, note_options, jobs).show(ui);
+
+ if let Some(note_action) = resp.action {
+ action = Some(note_action);
+ }
+ });
+
+ notedeck_ui::hline(ui);
+
+ RenderEntryResponse::Success(action)
+}
+
+fn render_reaction_cluster(
+ ui: &mut egui::Ui,
+ note_context: &mut NoteContext,
+ note_options: NoteOptions,
+ jobs: &mut JobsCache,
+ mute: &std::sync::Arc<Muted>,
+ txn: &Transaction,
+ reaction: &ReactionUnit,
+) -> RenderEntryResponse {
+ let reacted_to_key = reaction.note_reacted_to.key;
+ let reacted_to_note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, reacted_to_key) {
+ note
+ } else {
+ warn!("failed to query note {:?}", reacted_to_key);
+ return RenderEntryResponse::Unsuccessful;
+ };
+
+ let profiles_to_show: Vec<ProfileEntry> = reaction
+ .reactions
+ .values()
+ .filter(|r| !mute.is_pk_muted(r.sender.bytes()))
+ .map(|r| &r.sender)
+ .map(|p| ProfileEntry {
+ record: note_context.ndb.get_profile_by_pubkey(txn, p.bytes()).ok(),
+ pk: p,
+ })
+ .collect();
+
+ let first_name = get_display_name(profiles_to_show.iter().find_map(|opt| opt.record.as_ref()))
+ .name()
+ .to_string();
+ 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| {
+ 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))
+ }
+ }
+ });
+ });
+ },
+ );
+
+ 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.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}"));
+ }
+ });
+ });
+
+ 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);
+ RenderEntryResponse::Success(action)
+}
+
+enum RenderEntryResponse {
+ Unsuccessful,
+ Success(Option<NoteAction>),
+}
+
+struct ProfileEntry<'a> {
+ record: Option<ProfileRecord<'a>>,
+ pk: &'a Pubkey,
}