notedeck

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

commit a6d91c43e46a22db155ae0f7d5eff346676dc1fa
parent 19fe3703d9568ca69ccdf578290bd6b3d795ed77
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 16 Sep 2025 11:28:20 -0700

Merge a bunch of fixes from kernel

PRs

* 1141
* 1137
* 1136

kernelkind (10):
      Revert "feat: transitively trust images from parent note"
      feat: enable transitive trust for repost
      fix `NoteUnits` front insertion logic
      fix: don't reset scroll position when switching toolbar
      fix: no longer make the scroll position jump oddly
      fix: repost desc text size on newline
      make `tabs_ui` return `InnerResponse`
      refactor: impl transitive trust via `NoteOptions::TrustMedia`
      refactor: move `profile_body` to fn
      refactor: remove unnecessary code

Diffstat:
Mcrates/notedeck_columns/src/timeline/mod.rs | 24+++++++++++++++++++++---
Mcrates/notedeck_columns/src/timeline/note_units.rs | 41+++++++++++++++++++++++++----------------
Mcrates/notedeck_columns/src/timeline/route.rs | 1-
Mcrates/notedeck_columns/src/ui/note/post.rs | 2+-
Mcrates/notedeck_columns/src/ui/profile/mod.rs | 267++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcrates/notedeck_columns/src/ui/timeline.rs | 46+++++++++++++++++++++++++++-------------------
Mcrates/notedeck_ui/src/note/contents.rs | 52+++++++++-------------------------------------------
Mcrates/notedeck_ui/src/note/media.rs | 3+--
Mcrates/notedeck_ui/src/note/mod.rs | 48+++++++++++++++++++++---------------------------
Mcrates/notedeck_ui/src/note/options.rs | 3+++
10 files changed, 242 insertions(+), 245 deletions(-)

diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs @@ -133,6 +133,7 @@ impl TimelineTab { ndb: &Ndb, txn: &Transaction, reversed: bool, + use_front_insert: bool, ) -> Option<UnknownPks<'a>> { if payloads.is_empty() { return None; @@ -158,7 +159,11 @@ impl TimelineTab { debug!("spliced when inserting {num_refs} new notes, resetting virtual list",); list.reset(); } - MergeKind::FrontInsert => { + MergeKind::FrontInsert => 's: { + if !use_front_insert { + break 's; + } + // only run this logic if we're reverse-chronological // reversed in this case means chronological, since the // default is reverse-chronological. yeah it's confusing. @@ -210,6 +215,7 @@ pub struct Timeline { pub selected_view: usize, pub subscription: TimelineSub, + pub enable_front_insert: bool, } impl Timeline { @@ -271,12 +277,16 @@ impl Timeline { let subscription = TimelineSub::default(); let selected_view = 0; + // by default, disabled for profiles since they contain widgets above the list items + let enable_front_insert = !matches!(kind, TimelineKind::Profile(_)); + Timeline { kind, filter, views, subscription, selected_view, + enable_front_insert, } } @@ -402,7 +412,9 @@ impl Timeline { match view.filter { ViewFilter::NotesAndReplies => { let res: Vec<&NotePayload<'_>> = payloads.iter().collect(); - if let Some(res) = view.insert(res, ndb, txn, reversed) { + if let Some(res) = + view.insert(res, ndb, txn, reversed, self.enable_front_insert) + { res.process(unknown_ids, ndb, txn); } } @@ -418,7 +430,13 @@ impl Timeline { } } - if let Some(res) = view.insert(filtered_payloads, ndb, txn, reversed) { + if let Some(res) = view.insert( + filtered_payloads, + ndb, + txn, + reversed, + self.enable_front_insert, + ) { res.process(unknown_ids, ndb, txn); } } diff --git a/crates/notedeck_columns/src/timeline/note_units.rs b/crates/notedeck_columns/src/timeline/note_units.rs @@ -101,28 +101,37 @@ impl NoteUnits { let inserted_new = new_order.len(); - let front_insertion = inserted_new > 0 - && if self.order.is_empty() || new_order.is_empty() { - true - } else if !self.reversed { - let first_new = *new_order.first().unwrap(); - let last_old = *self.order.last().unwrap(); - self.storage[first_new] >= self.storage[last_old] - } else { - let last_new = *new_order.last().unwrap(); - let first_old = *self.order.first().unwrap(); - self.storage[last_new] <= self.storage[first_old] - }; + let front_insertion = if self.order.is_empty() || new_order.is_empty() { + !new_order.is_empty() + } else if self.reversed { + // reversed is true, sorting should occur less recent to most recent (oldest to newest, opposite of `self.order`) + let first_new = *new_order.first().unwrap(); // most recent unit of the new order + let last_old = *self.order.last().unwrap(); // least recent unit of the current order + + // if the most recent unit of the new order is less recent than the least recent unit of the current order, + // all current order units are less recent than the new order units. + // In other words, they are all being inserted in the front + self.storage[first_new] >= self.storage[last_old] + } else { + // reversed is false, sorting should occur most recent to least recent (newest to oldest, as it is in `self.order`) + let last_new = *new_order.last().unwrap(); // least recent unit of the new order + let first_old = *self.order.first().unwrap(); // most recent unit of the current order + + // if the least recent unit of the new order is more recent than the most recent unit of the current order, + // all new units are more recent than the current units. + // In other words, they are all being inserted in the front + self.storage[last_new] <= self.storage[first_old] + }; let mut merged = Vec::with_capacity(self.order.len() + new_order.len()); let (mut i, mut j) = (0, 0); while i < self.order.len() && j < new_order.len() { let index_left = self.order[i]; let index_right = new_order[j]; - let left_item = &self.storage[index_left]; - let right_item = &self.storage[index_right]; - if left_item <= right_item { - // left_item is newer than right_item + let left_unit = &self.storage[index_left]; + let right_unit = &self.storage[index_right]; + if left_unit <= right_unit { + // the left unit is more recent than (or the same recency as) the right unit merged.push(index_left); i += 1; } else { diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs @@ -31,7 +31,6 @@ pub fn render_timeline_route( | TimelineKind::Generic(_) => { let note_action = ui::TimelineView::new(kind, timeline_cache, note_context, note_options, jobs, col) - .scroll_to_top(scroll_to_top) .ui(ui); note_action.map(RenderNavAction::NoteAction) diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs @@ -410,7 +410,7 @@ impl<'a, 'd> PostView<'a, 'd> { self.note_context, txn, id.bytes(), - None, + nostrdb::NoteKey::new(0), self.note_options, self.jobs, ) diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -39,6 +39,11 @@ pub enum ProfileViewAction { Follow(Pubkey), } +struct ProfileScrollResponse { + body_end_pos: f32, + action: Option<ProfileViewAction>, +} + impl<'a, 'd> ProfileView<'a, 'd> { #[allow(clippy::too_many_arguments)] pub fn new( @@ -65,15 +70,13 @@ impl<'a, 'd> ProfileView<'a, 'd> { pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<ProfileViewAction> { let scroll_id = ProfileView::scroll_id(self.col_id, self.pubkey); - let offset_id = scroll_id.with("scroll_offset"); + let scroll_area = ScrollArea::vertical().id_salt(scroll_id).animated(false); - let mut scroll_area = ScrollArea::vertical().id_salt(scroll_id); - - if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) { - scroll_area = scroll_area.vertical_scroll_offset(offset); - } + let profile_timeline = self + .timeline_cache + .get_mut(&TimelineKind::Profile(*self.pubkey))?; - let output = scroll_area.show(ui, |ui| 's: { + let output = scroll_area.show(ui, |ui| { let mut action = None; let txn = Transaction::new(self.note_context.ndb).expect("txn"); let profile = self @@ -82,23 +85,19 @@ impl<'a, 'd> ProfileView<'a, 'd> { .get_profile_by_pubkey(&txn, self.pubkey.bytes()) .ok(); - if let Some(profile_view_action) = self.profile_body(ui, profile.as_ref()) { + if let Some(profile_view_action) = + profile_body(ui, self.pubkey, self.note_context, profile.as_ref()) + { action = Some(profile_view_action); } - let Some(profile_timeline) = self - .timeline_cache - .get_mut(&TimelineKind::Profile(*self.pubkey)) - else { - break 's action; - }; - - profile_timeline.selected_view = tabs_ui( + let tabs_resp = tabs_ui( ui, self.note_context.i18n, profile_timeline.selected_view, &profile_timeline.views, ); + profile_timeline.selected_view = tabs_resp.inner; let reversed = false; // poll for new notes and insert them into our existing notes @@ -124,145 +123,147 @@ impl<'a, 'd> ProfileView<'a, 'd> { action = Some(ProfileViewAction::Note(note_action)); } - action + ProfileScrollResponse { + body_end_pos: tabs_resp.response.rect.bottom(), + action, + } }); - ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y)); + // only allow front insert when the profile body is fully obstructed + profile_timeline.enable_front_insert = output.inner.body_end_pos < ui.clip_rect().top(); - output.inner + output.inner.action } +} - fn profile_body( - &mut self, - ui: &mut egui::Ui, - profile: Option<&ProfileRecord<'_>>, - ) -> Option<ProfileViewAction> { - let mut action = None; - ui.vertical(|ui| { - banner( - ui, - profile - .map(|p| p.record().profile()) - .and_then(|p| p.and_then(|p| p.banner())), - 120.0, - ); - - let padding = 12.0; - notedeck_ui::padding(padding, ui, |ui| { - let mut pfp_rect = ui.available_rect_before_wrap(); - let size = 80.0; - pfp_rect.set_width(size); - pfp_rect.set_height(size); - let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0)))); - - ui.horizontal(|ui| { - ui.put( - pfp_rect, - &mut ProfilePic::new(self.note_context.img_cache, get_profile_url(profile)) - .size(size) - .border(ProfilePic::border_stroke(ui)), - ); - - if ui - .add(copy_key_widget(&pfp_rect, self.note_context.i18n)) - .clicked() - { - let to_copy = if let Some(bech) = self.pubkey.npub() { - bech - } else { - error!("Could not convert Pubkey to bech"); - String::new() - }; - ui.ctx().copy_text(to_copy) - } +fn profile_body( + ui: &mut egui::Ui, + pubkey: &Pubkey, + note_context: &mut NoteContext, + profile: Option<&ProfileRecord<'_>>, +) -> Option<ProfileViewAction> { + let mut action = None; + ui.vertical(|ui| { + banner( + ui, + profile + .map(|p| p.record().profile()) + .and_then(|p| p.and_then(|p| p.banner())), + 120.0, + ); - ui.with_layout(Layout::right_to_left(egui::Align::RIGHT), |ui| { - ui.add_space(24.0); - - let target_key = self.pubkey; - let selected = self.note_context.accounts.get_selected_account(); - - let profile_type = if selected.key.secret_key.is_none() { - ProfileType::ReadOnly - } else if &selected.key.pubkey == self.pubkey { - ProfileType::MyProfile - } else { - ProfileType::Followable(selected.is_following(target_key.bytes())) - }; - - match profile_type { - ProfileType::MyProfile => { - if ui - .add(edit_profile_button(self.note_context.i18n)) - .clicked() - { - action = Some(ProfileViewAction::EditProfile); - } + let padding = 12.0; + notedeck_ui::padding(padding, ui, |ui| { + let mut pfp_rect = ui.available_rect_before_wrap(); + let size = 80.0; + pfp_rect.set_width(size); + pfp_rect.set_height(size); + let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0)))); + + ui.horizontal(|ui| { + ui.put( + pfp_rect, + &mut ProfilePic::new(note_context.img_cache, get_profile_url(profile)) + .size(size) + .border(ProfilePic::border_stroke(ui)), + ); + + if ui + .add(copy_key_widget(&pfp_rect, note_context.i18n)) + .clicked() + { + let to_copy = if let Some(bech) = pubkey.npub() { + bech + } else { + error!("Could not convert Pubkey to bech"); + String::new() + }; + ui.ctx().copy_text(to_copy) + } + + ui.with_layout(Layout::right_to_left(egui::Align::RIGHT), |ui| { + ui.add_space(24.0); + + let target_key = pubkey; + let selected = note_context.accounts.get_selected_account(); + + let profile_type = if selected.key.secret_key.is_none() { + ProfileType::ReadOnly + } else if &selected.key.pubkey == pubkey { + ProfileType::MyProfile + } else { + ProfileType::Followable(selected.is_following(target_key.bytes())) + }; + + match profile_type { + ProfileType::MyProfile => { + if ui.add(edit_profile_button(note_context.i18n)).clicked() { + action = Some(ProfileViewAction::EditProfile); } - ProfileType::Followable(is_following) => { - let follow_button = ui.add(follow_button(is_following)); - - if follow_button.clicked() { - action = match is_following { - IsFollowing::Unknown => { - // don't do anything, we don't have contact list - None - } - - IsFollowing::Yes => { - Some(ProfileViewAction::Unfollow(target_key.to_owned())) - } - - IsFollowing::No => { - Some(ProfileViewAction::Follow(target_key.to_owned())) - } - }; - } + } + ProfileType::Followable(is_following) => { + let follow_button = ui.add(follow_button(is_following)); + + if follow_button.clicked() { + action = match is_following { + IsFollowing::Unknown => { + // don't do anything, we don't have contact list + None + } + + IsFollowing::Yes => { + Some(ProfileViewAction::Unfollow(target_key.to_owned())) + } + + IsFollowing::No => { + Some(ProfileViewAction::Follow(target_key.to_owned())) + } + }; } - ProfileType::ReadOnly => {} } - }); + ProfileType::ReadOnly => {} + } }); + }); - ui.add_space(18.0); + ui.add_space(18.0); - ui.add(display_name_widget(&get_display_name(profile), false)); + ui.add(display_name_widget(&get_display_name(profile), false)); - ui.add_space(8.0); + ui.add_space(8.0); - ui.add(about_section_widget(profile)); + ui.add(about_section_widget(profile)); - ui.horizontal_wrapped(|ui| { - let website_url = profile - .as_ref() - .map(|p| p.record().profile()) - .and_then(|p| p.and_then(|p| p.website()).filter(|s| !s.is_empty())); + ui.horizontal_wrapped(|ui| { + let website_url = profile + .as_ref() + .map(|p| p.record().profile()) + .and_then(|p| p.and_then(|p| p.website()).filter(|s| !s.is_empty())); - let lud16 = profile - .as_ref() - .map(|p| p.record().profile()) - .and_then(|p| p.and_then(|p| p.lud16()).filter(|s| !s.is_empty())); + let lud16 = profile + .as_ref() + .map(|p| p.record().profile()) + .and_then(|p| p.and_then(|p| p.lud16()).filter(|s| !s.is_empty())); - if let Some(website_url) = website_url { - ui.horizontal(|ui| { - handle_link(ui, website_url); - }); - } + if let Some(website_url) = website_url { + ui.horizontal(|ui| { + handle_link(ui, website_url); + }); + } - if let Some(lud16) = lud16 { - if website_url.is_some() { - ui.end_row(); - } - ui.horizontal(|ui| { - handle_lud16(ui, lud16); - }); + if let Some(lud16) = lud16 { + if website_url.is_some() { + ui.end_row(); } - }); + ui.horizontal(|ui| { + handle_lud16(ui, lud16); + }); + } }); }); + }); - action - } + action } enum ProfileType { diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs @@ -1,8 +1,9 @@ use egui::containers::scroll_area::ScrollBarVisibility; -use egui::{vec2, Color32, Direction, Layout, Margin, Pos2, ScrollArea, Sense, Stroke}; +use egui::{vec2, Color32, Direction, Layout, Margin, Pos2, RichText, ScrollArea, Sense, Stroke}; use egui_tabs::TabColor; use enostr::Pubkey; use nostrdb::{Note, ProfileRecord, Transaction}; +use notedeck::fonts::get_font_size; use notedeck::name::get_display_name; use notedeck::ui::is_narrow; use notedeck::{tr_plural, JobsCache, Muted, NotedeckTextStyle}; @@ -118,7 +119,8 @@ fn timeline_ui( note_context.i18n, timeline.selected_view, &timeline.views, - ); + ) + .inner; // need this for some reason?? ui.add_space(3.0); @@ -151,12 +153,6 @@ fn timeline_ui( .auto_shrink([false, false]) .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible); - let offset_id = scroll_id.with("timeline_scroll_offset"); - - if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) { - scroll_area = scroll_area.vertical_scroll_offset(offset); - } - if goto_top_resp.is_some_and(|r| r.clicked()) { scroll_area = scroll_area.vertical_scroll_offset(0.0); } @@ -195,8 +191,6 @@ fn timeline_ui( .show(ui) }); - ui.data_mut(|d| d.insert_temp(offset_id, scroll_output.state.offset.y)); - let at_top_after_scroll = scroll_output.state.offset.y == 0.0; let cur_show_top_button = ui.ctx().data(|d| d.get_temp::<bool>(show_top_button_id)); @@ -284,7 +278,7 @@ pub fn tabs_ui( i18n: &mut Localization, selected: usize, views: &[TimelineTab], -) -> usize { +) -> egui::InnerResponse<usize> { ui.spacing_mut().item_spacing.y = 0.0; let tab_res = egui_tabs::Tabs::new(views.len() as i32) @@ -332,7 +326,9 @@ pub fn tabs_ui( let sel = tab_res.selected().unwrap_or_default(); - let (underline, underline_y) = tab_res.inner()[sel as usize].inner; + let res_inner = &tab_res.inner()[sel as usize]; + + let (underline, underline_y) = res_inner.inner; let underline_width = underline.span(); let tab_anim_id = ui.id().with("tab_anim"); @@ -359,7 +355,7 @@ pub fn tabs_ui( ui.painter().hline(underline, underline_y, stroke); - sel as usize + egui::InnerResponse::new(sel as usize, res_inner.response.clone()) } fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 { @@ -734,7 +730,7 @@ fn render_reaction_cluster( fn render_composite_entry( ui: &mut egui::Ui, note_context: &mut NoteContext, - note_options: NoteOptions, + mut note_options: NoteOptions, jobs: &mut JobsCache, underlying_note: &nostrdb::Note<'_>, profiles_to_show: Vec<ProfileEntry>, @@ -760,6 +756,16 @@ fn render_composite_entry( ReferencedNoteType::Yours }; + if !note_options.contains(NoteOptions::TrustMedia) { + let acc = note_context.accounts.get_selected_account(); + for entry in &profiles_to_show { + if matches!(acc.is_following(entry.pk), notedeck::IsFollowing::Yes) { + note_options = note_options.union(NoteOptions::TrustMedia); + break; + } + } + } + egui::Frame::new() .inner_margin(Margin::symmetric(8, 4)) .show(ui, |ui| { @@ -829,7 +835,10 @@ fn render_composite_entry( ui.horizontal(|ui| { ui.add_space(48.0); ui.horizontal_wrapped(|ui| { - ui.label(desc); + ui.add(egui::Label::new( + RichText::new(desc) + .size(get_font_size(ui.ctx(), &NotedeckTextStyle::Small)), + )); }); }); } @@ -838,15 +847,14 @@ fn render_composite_entry( let resp = ui .horizontal(|ui| { - let mut options = note_options; - if options.contains(NoteOptions::Notification) { - options = options + if note_options.contains(NoteOptions::Notification) { + note_options = note_options .difference(NoteOptions::ActionBar | NoteOptions::OptionsButton) .union(NoteOptions::NotificationPreview); ui.add_space(48.0); }; - NoteView::new(note_context, underlying_note, options, jobs).show(ui) + NoteView::new(note_context, underlying_note, note_options, jobs).show(ui) }) .inner; diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs @@ -4,11 +4,9 @@ use crate::{ secondary_label, }; use egui::{Color32, Hyperlink, Label, RichText}; -use nostrdb::{BlockType, Mention, Note, Transaction}; +use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use notedeck::Localization; -use notedeck::{ - time_format, update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle, -}; +use notedeck::{time_format, update_imeta_blurhashes, NoteCache, NoteContext, NotedeckTextStyle}; use notedeck::{JobsCache, RenderableMedia}; use tracing::warn; @@ -16,7 +14,6 @@ pub struct NoteContents<'a, 'd> { note_context: &'a mut NoteContext<'d>, txn: &'a Transaction, note: &'a Note<'a>, - parent: Option<&'a Note<'a>>, options: NoteOptions, pub action: Option<NoteAction>, jobs: &'a mut JobsCache, @@ -28,7 +25,6 @@ impl<'a, 'd> NoteContents<'a, 'd> { note_context: &'a mut NoteContext<'d>, txn: &'a Transaction, note: &'a Note, - parent: Option<&'a Note>, options: NoteOptions, jobs: &'a mut JobsCache, ) -> Self { @@ -36,7 +32,6 @@ impl<'a, 'd> NoteContents<'a, 'd> { note_context, txn, note, - parent, options, action: None, jobs, @@ -51,7 +46,6 @@ impl egui::Widget for &mut NoteContents<'_, '_> { self.note_context, self.txn, self.note, - self.parent, self.options, self.jobs, ); @@ -87,7 +81,7 @@ pub fn render_note_preview( note_context: &mut NoteContext, txn: &Transaction, id: &[u8; 32], - parent: Option<&Note>, + parent: NoteKey, note_options: NoteOptions, jobs: &mut JobsCache, ) -> NoteResponse { @@ -118,12 +112,10 @@ pub fn render_note_preview( */ }; - let mut view = NoteView::new(note_context, &note, note_options, jobs).preview_style(); - if let Some(parent) = parent { - view = view.parent(parent); - } - - view.show(ui) + NoteView::new(note_context, &note, note_options, jobs) + .preview_style() + .parent(parent) + .show(ui) } /// Render note contents and surrounding info (client name, full date timestamp) @@ -132,12 +124,10 @@ fn render_note_contents( note_context: &mut NoteContext, txn: &Transaction, note: &Note, - parent: Option<&Note>, options: NoteOptions, jobs: &mut JobsCache, ) -> NoteResponse { - let response = - render_undecorated_note_contents(ui, note_context, txn, note, parent, options, jobs); + let response = render_undecorated_note_contents(ui, note_context, txn, note, options, jobs); ui.horizontal_wrapped(|ui| { note_bottom_metadata_ui( @@ -178,7 +168,6 @@ fn render_undecorated_note_contents<'a>( note_context: &mut NoteContext, txn: &Transaction, note: &'a Note, - parent: Option<&'a Note>, options: NoteOptions, jobs: &mut JobsCache, ) -> NoteResponse { @@ -366,7 +355,7 @@ fn render_undecorated_note_contents<'a>( }); let preview_note_action = inline_note.and_then(|(id, _)| { - render_note_preview(ui, note_context, txn, id, Some(note), options, jobs) + render_note_preview(ui, note_context, txn, id, note_key, options, jobs) .action .map(|a| match a { NoteAction::Note { note_id, .. } => NoteAction::Note { @@ -383,28 +372,6 @@ fn render_undecorated_note_contents<'a>( ui.add_space(2.0); let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note"))); - let is_self = note.pubkey() - == note_context - .accounts - .get_selected_account() - .key - .pubkey - .bytes(); - - let trusted_media = { - let is_followed = |pk| { - matches!( - note_context - .accounts - .get_selected_account() - .is_following(pk), - IsFollowing::Yes - ) - }; - - is_self || is_followed(note.pubkey()) || parent.is_some_and(|p| is_followed(p.pubkey())) - }; - media_action = image_carousel( ui, note_context.img_cache, @@ -412,7 +379,6 @@ fn render_undecorated_note_contents<'a>( jobs, &supported_medias, carousel_id, - trusted_media, note_context.i18n, options, ); diff --git a/crates/notedeck_ui/src/note/media.rs b/crates/notedeck_ui/src/note/media.rs @@ -33,7 +33,6 @@ pub fn image_carousel( jobs: &mut JobsCache, medias: &[RenderableMedia], carousel_id: egui::Id, - trusted_media: bool, i18n: &mut Localization, note_options: NoteOptions, ) -> Option<MediaAction> { @@ -68,7 +67,7 @@ pub fn image_carousel( job_pool, jobs, media, - trusted_media, + note_options.contains(NoteOptions::TrustMedia), i18n, size, if note_options.contains(NoteOptions::NoAnimations) { diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs @@ -32,7 +32,7 @@ use notedeck::{ pub struct NoteView<'a, 'd> { note_context: &'a mut NoteContext<'d>, - parent: Option<&'a Note<'a>>, + parent: Option<NoteKey>, note: &'a nostrdb::Note<'a>, flags: NoteOptions, jobs: &'a mut JobsCache, @@ -85,7 +85,7 @@ impl<'a, 'd> NoteView<'a, 'd> { flags: NoteOptions, jobs: &'a mut JobsCache, ) -> Self { - let parent: Option<&Note> = None; + let parent: Option<NoteKey> = None; Self { note_context, @@ -209,7 +209,7 @@ impl<'a, 'd> NoteView<'a, 'd> { } #[inline] - pub fn parent(mut self, parent: &'a Note<'a>) -> Self { + pub fn parent(mut self, parent: NoteKey) -> Self { self.parent = Some(parent); self } @@ -255,7 +255,6 @@ impl<'a, 'd> NoteView<'a, 'd> { self.note_context, txn, self.note, - self.parent, self.flags, self.jobs, )); @@ -303,6 +302,18 @@ impl<'a, 'd> NoteView<'a, 'd> { } pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse { + if !self.flags.contains(NoteOptions::TrustMedia) { + let acc = self.note_context.accounts.get_selected_account(); + if self.note.pubkey() == acc.key.pubkey.bytes() + || matches!( + acc.is_following(self.note.pubkey()), + notedeck::IsFollowing::Yes + ) + { + self.flags = self.flags.union(NoteOptions::TrustMedia); + } + } + if self.options().contains(NoteOptions::Textmode) { NoteResponse::new(self.textmode_ui(ui)) } else if self.options().contains(NoteOptions::Framed) { @@ -426,14 +437,8 @@ impl<'a, 'd> NoteView<'a, 'd> { }); } - let mut contents = NoteContents::new( - self.note_context, - txn, - self.note, - self.parent, - self.flags, - self.jobs, - ); + let mut contents = + NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs); ui.add(&mut contents); @@ -526,14 +531,8 @@ impl<'a, 'd> NoteView<'a, 'd> { }); } - let mut contents = NoteContents::new( - self.note_context, - txn, - self.note, - self.parent, - self.flags, - self.jobs, - ); + let mut contents = + NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs); ui.add(&mut contents); note_action = contents.action.or(note_action); @@ -577,12 +576,7 @@ impl<'a, 'd> NoteView<'a, 'd> { .ndb .get_profile_by_pubkey(txn, self.note.pubkey()); - let hitbox_id = note_hitbox_id( - note_key, - self.options(), - self.parent - .map(|n| n.key().expect("todo: support non-db notes")), - ); + let hitbox_id = note_hitbox_id(note_key, self.options(), self.parent); let maybe_hitbox = maybe_note_hitbox(ui, hitbox_id); // wide design @@ -749,7 +743,7 @@ fn note_hitbox_id( fn maybe_note_hitbox(ui: &mut egui::Ui, hitbox_id: egui::Id) -> Option<Response> { ui.ctx() - .data_mut(|d| d.get_persisted(hitbox_id)) + .data_mut(|d| d.get_temp(hitbox_id)) .map(|note_size: Vec2| { // The hitbox should extend the entire width of the // container. The hitbox height was cached last layout. diff --git a/crates/notedeck_ui/src/note/options.rs b/crates/notedeck_ui/src/note/options.rs @@ -44,6 +44,9 @@ bitflags! { /// The note is a notification const Notification = 1 << 19; + + /// There is enough trust to show media in this note + const TrustMedia = 1 << 20; } }