thread UI

Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
kernelkind
2025-06-16 17:55:47 -04:00
parent 51476772c4
commit b3569e90d6

View File

@@ -1,10 +1,15 @@
use egui::InnerResponse;
use egui_virtual_list::VirtualList;
use enostr::KeypairUnowned;
use nostrdb::Transaction;
use nostrdb::{Note, Transaction};
use notedeck::note::root_note_id_from_selected_id;
use notedeck::{MuteFun, NoteAction, NoteContext, RootNoteId, UnknownIds};
use notedeck_ui::jobs::JobsCache;
use notedeck_ui::NoteOptions;
use notedeck_ui::note::NoteResponse;
use notedeck_ui::{NoteOptions, NoteView};
use tracing::error;
use crate::timeline::thread::NoteSeenFlags;
use crate::timeline::{ThreadSelection, TimelineCache, TimelineKind};
use crate::ui::timeline::TimelineTabView;
@@ -60,7 +65,9 @@ impl<'a, 'd> ThreadView<'a, 'd> {
.auto_shrink([false, false])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible);
let offset_id = self.id_source.with("scroll_offset");
let offset_id = self
.id_source
.with(("scroll_offset", self.selected_note_id));
if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) {
scroll_area = scroll_area.vertical_scroll_offset(offset);
@@ -123,3 +130,258 @@ impl<'a, 'd> ThreadView<'a, 'd> {
output.inner
}
}
#[allow(clippy::too_many_arguments)]
fn show_notes(
ui: &mut egui::Ui,
list: &mut VirtualList,
thread_notes: &ThreadNotes,
note_context: &mut NoteContext<'_>,
zapping_acc: Option<&KeypairUnowned<'_>>,
flags: NoteOptions,
jobs: &mut JobsCache,
txn: &Transaction,
is_muted: &MuteFun,
) -> Option<NoteAction> {
let mut action = None;
ui.spacing_mut().item_spacing.y = 0.0;
ui.spacing_mut().item_spacing.x = 4.0;
let selected_note_index = thread_notes.selected_index;
let notes = &thread_notes.notes;
list.ui_custom_layout(ui, notes.len(), |ui, cur_index| 's: {
let note = &notes[cur_index];
// should we mute the thread? we might not have it!
let muted = root_note_id_from_selected_id(
note_context.ndb,
note_context.note_cache,
txn,
note.note.id(),
)
.ok()
.is_some_and(|root_id| is_muted(&note.note, root_id.bytes()));
if muted {
break 's 0;
}
let resp = note.show(note_context, zapping_acc, flags, jobs, ui);
action = if cur_index == selected_note_index {
resp.action.and_then(strip_note_action)
} else {
resp.action
}
.or(action.take());
1
});
action
}
fn strip_note_action(action: NoteAction) -> Option<NoteAction> {
if matches!(action, NoteAction::Note(_)) {
return None;
}
Some(action)
}
struct ThreadNoteBuilder<'a> {
chain: Vec<Note<'a>>,
selected: Note<'a>,
replies: Vec<Note<'a>>,
}
impl<'a> ThreadNoteBuilder<'a> {
pub fn new(selected: Note<'a>) -> Self {
Self {
chain: Vec::new(),
selected,
replies: Vec::new(),
}
}
pub fn add_chain(&mut self, note: Note<'a>) {
self.chain.push(note);
}
pub fn add_reply(&mut self, note: Note<'a>) {
self.replies.push(note);
}
pub fn into_notes(mut self, seen_flags: &mut NoteSeenFlags) -> ThreadNotes<'a> {
let mut notes = Vec::new();
let selected_is_root = self.chain.is_empty();
let mut cur_is_root = true;
while let Some(note) = self.chain.pop() {
notes.push(ThreadNote {
unread_and_have_replies: *seen_flags.get(note.id()).unwrap_or(&false),
note,
note_type: ThreadNoteType::Chain { root: cur_is_root },
});
cur_is_root = false;
}
let selected_index = notes.len();
notes.push(ThreadNote {
note: self.selected,
note_type: ThreadNoteType::Selected {
root: selected_is_root,
},
unread_and_have_replies: false,
});
for reply in self.replies {
notes.push(ThreadNote {
unread_and_have_replies: *seen_flags.get(reply.id()).unwrap_or(&false),
note: reply,
note_type: ThreadNoteType::Reply,
});
}
ThreadNotes {
notes,
selected_index,
}
}
}
enum ThreadNoteType {
Chain { root: bool },
Selected { root: bool },
Reply,
}
struct ThreadNotes<'a> {
notes: Vec<ThreadNote<'a>>,
selected_index: usize,
}
struct ThreadNote<'a> {
pub note: Note<'a>,
note_type: ThreadNoteType,
pub unread_and_have_replies: bool,
}
impl<'a> ThreadNote<'a> {
fn options(&self, mut cur_options: NoteOptions) -> NoteOptions {
match self.note_type {
ThreadNoteType::Chain { root: _ } => cur_options,
ThreadNoteType::Selected { root: _ } => {
cur_options.set_wide(true);
cur_options
}
ThreadNoteType::Reply => cur_options,
}
}
fn show(
&self,
note_context: &'a mut NoteContext<'_>,
zapping_acc: Option<&'a KeypairUnowned<'a>>,
flags: NoteOptions,
jobs: &'a mut JobsCache,
ui: &mut egui::Ui,
) -> NoteResponse {
let inner = notedeck_ui::padding(8.0, ui, |ui| {
NoteView::new(
note_context,
zapping_acc,
&self.note,
self.options(flags),
jobs,
)
.unread_indicator(self.unread_and_have_replies)
.show(ui)
});
match self.note_type {
ThreadNoteType::Chain { root } => add_chain_adornment(ui, &inner, root),
ThreadNoteType::Selected { root } => add_selected_adornment(ui, &inner, root),
ThreadNoteType::Reply => notedeck_ui::hline(ui),
}
inner.inner
}
}
fn add_chain_adornment(ui: &mut egui::Ui, note_resp: &InnerResponse<NoteResponse>, root: bool) {
let Some(pfp_rect) = note_resp.inner.pfp_rect else {
return;
};
let note_rect = note_resp.response.rect;
let painter = ui.painter_at(note_rect);
if !root {
paint_line_above_pfp(ui, &painter, &pfp_rect, &note_rect);
}
// painting line below pfp:
let top_pt = {
let mut top = pfp_rect.center();
top.y = pfp_rect.bottom();
top
};
let bottom_pt = {
let mut bottom = top_pt;
bottom.y = note_rect.bottom();
bottom
};
painter.line_segment([top_pt, bottom_pt], LINE_STROKE(ui));
let hline_min_x = top_pt.x + 6.0;
notedeck_ui::hline_with_width(
ui,
egui::Rangef::new(hline_min_x, ui.available_rect_before_wrap().right()),
);
}
fn add_selected_adornment(ui: &mut egui::Ui, note_resp: &InnerResponse<NoteResponse>, root: bool) {
let Some(pfp_rect) = note_resp.inner.pfp_rect else {
return;
};
let note_rect = note_resp.response.rect;
let painter = ui.painter_at(note_rect);
if !root {
paint_line_above_pfp(ui, &painter, &pfp_rect, &note_rect);
}
notedeck_ui::hline(ui);
}
fn paint_line_above_pfp(
ui: &egui::Ui,
painter: &egui::Painter,
pfp_rect: &egui::Rect,
note_rect: &egui::Rect,
) {
let bottom_pt = {
let mut center = pfp_rect.center();
center.y = pfp_rect.top();
center
};
let top_pt = {
let mut top = bottom_pt;
top.y = note_rect.top();
top
};
painter.line_segment([bottom_pt, top_pt], LINE_STROKE(ui));
}
const LINE_STROKE: fn(&egui::Ui) -> egui::Stroke = |ui: &egui::Ui| {
let mut stroke = ui.style().visuals.widgets.noninteractive.bg_stroke;
stroke.width = 2.0;
stroke
};