Merge show-note-client option by fernando

We should move this somewhere else before we turn it on
officially

Fernando López Guevara (2):
      refactor: use Margin:ZERO
      feat(note-view): show note client
This commit is contained in:
William Casarin
2025-07-16 14:00:14 -07:00
15 changed files with 115 additions and 60 deletions

View File

@@ -8,6 +8,7 @@ use crate::{
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler, DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler,
UnknownIds, UnknownIds,
}; };
use egui::Margin;
use egui::ThemePreference; use egui::ThemePreference;
use egui_winit::clipboard::Clipboard; use egui_winit::clipboard::Clipboard;
use enostr::RelayPool; use enostr::RelayPool;
@@ -51,14 +52,8 @@ pub struct Notedeck {
/// Our chrome, which is basically nothing /// Our chrome, which is basically nothing
fn main_panel(style: &egui::Style) -> egui::CentralPanel { fn main_panel(style: &egui::Style) -> egui::CentralPanel {
let inner_margin = egui::Margin {
top: 0,
left: 0,
right: 0,
bottom: 0,
};
egui::CentralPanel::default().frame(egui::Frame { egui::CentralPanel::default().frame(egui::Frame {
inner_margin, inner_margin: Margin::ZERO,
fill: style.visuals.panel_fill, fill: style.visuals.panel_fill,
..Default::default() ..Default::default()
}) })

View File

@@ -6,6 +6,7 @@ use tracing::error;
pub struct Args { pub struct Args {
pub relays: Vec<String>, pub relays: Vec<String>,
pub is_mobile: Option<bool>, pub is_mobile: Option<bool>,
pub show_note_client: bool,
pub keys: Vec<Keypair>, pub keys: Vec<Keypair>,
pub light: bool, pub light: bool,
pub debug: bool, pub debug: bool,
@@ -28,6 +29,7 @@ impl Args {
is_mobile: None, is_mobile: None,
keys: vec![], keys: vec![],
light: false, light: false,
show_note_client: false,
debug: false, debug: false,
relay_debug: false, relay_debug: false,
tests: false, tests: false,
@@ -116,6 +118,8 @@ impl Args {
res.use_keystore = false; res.use_keystore = false;
} else if arg == "--relay-debug" { } else if arg == "--relay-debug" {
res.relay_debug = true; res.relay_debug = true;
} else if arg == "--show-note-client" {
res.show_note_client = true;
} else { } else {
unrecognized_args.insert(arg.clone()); unrecognized_args.insert(arg.clone());
} }

View File

@@ -193,3 +193,19 @@ where
|rnid| Ok(RootNoteId::new_unsafe(rnid.id)), |rnid| Ok(RootNoteId::new_unsafe(rnid.id)),
) )
} }
pub fn event_tag<'a>(ev: &nostrdb::Note<'a>, name: &str) -> Option<&'a str> {
ev.tags().iter().find_map(|tag| {
if tag.count() < 2 {
return None;
}
let cur_name = tag.get_str(0)?;
if cur_name != name {
return None;
}
tag.get_str(1)
})
}

View File

@@ -33,18 +33,28 @@ impl NoteCache {
#[derive(Clone)] #[derive(Clone)]
pub struct CachedNote { pub struct CachedNote {
reltime: TimeCached<String>, reltime: TimeCached<String>,
pub client: Option<String>,
pub reply: NoteReplyBuf, pub reply: NoteReplyBuf,
} }
impl CachedNote { impl CachedNote {
pub fn new(note: &Note<'_>) -> Self { pub fn new(note: &Note) -> Self {
use crate::note::event_tag;
let created_at = note.created_at(); let created_at = note.created_at();
let reltime = TimeCached::new( let reltime = TimeCached::new(
Duration::from_secs(1), Duration::from_secs(1),
Box::new(move || time_ago_since(created_at)), Box::new(move || time_ago_since(created_at)),
); );
let reply = NoteReply::new(note.tags()).to_owned(); let reply = NoteReply::new(note.tags()).to_owned();
CachedNote { reltime, reply }
let client = event_tag(note, "client");
CachedNote {
client: client.map(|c| c.to_string()),
reltime,
reply,
}
} }
pub fn reltime_str_mut(&mut self) -> &str { pub fn reltime_str_mut(&mut self) -> &str {

View File

@@ -64,23 +64,6 @@ impl Zap {
} }
} }
#[allow(dead_code)]
pub fn event_tag<'a>(ev: nostrdb::Note<'a>, name: &str) -> Option<&'a str> {
ev.tags().iter().find_map(|tag| {
if tag.count() < 2 {
return None;
}
let cur_name = tag.get_str(0)?;
if cur_name != name {
return None;
}
tag.get_str(1)
})
}
fn determine_zap_target(tags: &ZapTags) -> Option<ZapTarget> { fn determine_zap_target(tags: &ZapTags) -> Option<ZapTarget> {
if let Some(note_zapped) = tags.note_zapped { if let Some(note_zapped) = tags.note_zapped {
Some(ZapTarget::Note(NoteZapTarget { Some(ZapTarget::Note(NoteZapTarget {

View File

@@ -9,7 +9,6 @@ use notedeck::{App, AppAction, AppContext, NotedeckTextStyle, UserAccount, Walle
use notedeck_columns::{ use notedeck_columns::{
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus, column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
}; };
use notedeck_dave::{Dave, DaveAvatar}; use notedeck_dave::{Dave, DaveAvatar};
use notedeck_ui::{app_images, AnimationHelper, ProfilePic}; use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
@@ -289,7 +288,7 @@ impl Chrome {
/// How far is the chrome panel expanded? /// How far is the chrome panel expanded?
fn amount_open(&self, ui: &mut egui::Ui) -> f32 { fn amount_open(&self, ui: &mut egui::Ui) -> f32 {
let open_id = egui::Id::new("chrome_open"); let open_id = egui::Id::new("chrome_open");
let side_panel_width: f32 = 70.0; let side_panel_width: f32 = 74.0;
ui.ctx().animate_bool(open_id, self.open) * side_panel_width ui.ctx().animate_bool(open_id, self.open) * side_panel_width
} }
@@ -406,7 +405,7 @@ impl Chrome {
// macos needs a bit of space to make room for window // macos needs a bit of space to make room for window
// minimize/close buttons // minimize/close buttons
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
ui.add_space(28.0); ui.add_space(30.0);
} else { } else {
// we still want *some* padding so that it aligns with the + button regardless // we still want *some* padding so that it aligns with the + button regardless
ui.add_space(notedeck_ui::constants::FRAME_MARGIN.into()); ui.add_space(notedeck_ui::constants::FRAME_MARGIN.into());
@@ -615,11 +614,12 @@ fn wallet_button() -> impl Widget {
let max_size = img_size * ICON_EXPANSION_MULTIPLE; let max_size = img_size * ICON_EXPANSION_MULTIPLE;
let mut img = app_images::wallet_image().max_width(img_size); let img = if !ui.visuals().dark_mode {
app_images::wallet_light_image()
if !ui.visuals().dark_mode { } else {
img = img.tint(egui::Color32::BLACK); app_images::wallet_dark_image()
} }
.max_width(img_size);
let helper = AnimationHelper::new(ui, "wallet-icon", vec2(max_size, max_size)); let helper = AnimationHelper::new(ui, "wallet-icon", vec2(max_size, max_size));

View File

@@ -452,6 +452,10 @@ impl Damus {
NoteOptions::HideMedia, NoteOptions::HideMedia,
parsed_args.is_flag_set(ColumnsFlag::NoMedia), parsed_args.is_flag_set(ColumnsFlag::NoMedia),
); );
note_options.set(
NoteOptions::ShowNoteClient,
parsed_args.is_flag_set(ColumnsFlag::ShowNoteClient),
);
options.set(AppOptions::Debug, ctx.args.debug); options.set(AppOptions::Debug, ctx.args.debug);
options.set( options.set(
AppOptions::SinceOptimize, AppOptions::SinceOptimize,

View File

@@ -11,6 +11,7 @@ pub enum ColumnsFlag {
Textmode, Textmode,
Scramble, Scramble,
NoMedia, NoMedia,
ShowNoteClient,
} }
pub struct ColumnsArgs { pub struct ColumnsArgs {
@@ -52,6 +53,8 @@ impl ColumnsArgs {
res.clear_flag(ColumnsFlag::SinceOptimize); res.clear_flag(ColumnsFlag::SinceOptimize);
} else if arg == "--scramble" { } else if arg == "--scramble" {
res.set_flag(ColumnsFlag::Scramble); res.set_flag(ColumnsFlag::Scramble);
} else if arg == "--show-note-client" {
res.set_flag(ColumnsFlag::ShowNoteClient);
} else if arg == "--no-media" { } else if arg == "--no-media" {
res.set_flag(ColumnsFlag::NoMedia); res.set_flag(ColumnsFlag::NoMedia);
} else if arg == "--filter" { } else if arg == "--filter" {

View File

@@ -643,15 +643,17 @@ fn media_upload_button() -> impl egui::Widget {
painter.rect_filled(resp.rect, 8.0, fill_color); painter.rect_filled(resp.rect, 8.0, fill_color);
painter.rect_stroke(resp.rect, 8.0, stroke, egui::StrokeKind::Middle); painter.rect_stroke(resp.rect, 8.0, stroke, egui::StrokeKind::Middle);
let mut upload_img = app_images::media_upload_dark_image();
if !ui.visuals().dark_mode { let upload_img = if ui.visuals().dark_mode {
upload_img = upload_img.tint(egui::Color32::BLACK); app_images::media_upload_dark_image()
} else {
app_images::media_upload_light_image()
}; };
upload_img upload_img
.max_size(egui::vec2(16.0, 16.0)) .max_size(egui::vec2(16.0, 16.0))
.paint_at(ui, resp.rect.shrink(8.0)); .paint_at(ui, resp.rect.shrink(8.0));
resp resp
} }
} }

View File

@@ -261,9 +261,9 @@ enum ProfileType {
fn handle_link(ui: &mut egui::Ui, website_url: &str) { fn handle_link(ui: &mut egui::Ui, website_url: &str) {
let img = if ui.visuals().dark_mode { let img = if ui.visuals().dark_mode {
app_images::link_image() app_images::link_dark_image()
} else { } else {
app_images::link_image().tint(egui::Color32::BLACK) app_images::link_light_image()
}; };
ui.add(img); ui.add(img);

View File

@@ -1,5 +1,5 @@
use eframe::icon_data::from_png_bytes; use eframe::icon_data::from_png_bytes;
use egui::{include_image, IconData, Image}; use egui::{include_image, Color32, IconData, Image};
pub fn app_icon() -> IconData { pub fn app_icon() -> IconData {
from_png_bytes(include_bytes!("../../../assets/damus-app-icon.png")).expect("icon") from_png_bytes(include_bytes!("../../../assets/damus-app-icon.png")).expect("icon")
@@ -113,12 +113,12 @@ pub fn help_light_image() -> Image<'static> {
)) ))
} }
pub fn home_dark_image() -> Image<'static> { pub fn home_light_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/home-toolbar.png")) home_dark_image().tint(Color32::BLACK)
} }
pub fn home_light_image() -> Image<'static> { pub fn home_dark_image() -> Image<'static> {
home_dark_image().tint(egui::Color32::BLACK) Image::new(include_image!("../../../assets/icons/home-toolbar.png"))
} }
pub fn home_image() -> Image<'static> { pub fn home_image() -> Image<'static> {
@@ -131,10 +131,14 @@ pub fn key_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/key_4x.png")) Image::new(include_image!("../../../assets/icons/key_4x.png"))
} }
pub fn link_image() -> Image<'static> { pub fn link_dark_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/links_4x.png")) Image::new(include_image!("../../../assets/icons/links_4x.png"))
} }
pub fn link_light_image() -> Image<'static> {
link_dark_image().tint(Color32::BLACK)
}
pub fn new_message_image() -> Image<'static> { pub fn new_message_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/newmessage_64.png")) Image::new(include_image!("../../../assets/icons/newmessage_64.png"))
} }
@@ -153,15 +157,16 @@ pub fn notifications_image(dark_mode: bool) -> Image<'static> {
} }
} }
pub fn notifications_light_image() -> Image<'static> {
notifications_dark_image().tint(Color32::BLACK)
}
pub fn notifications_dark_image() -> Image<'static> { pub fn notifications_dark_image() -> Image<'static> {
Image::new(include_image!( Image::new(include_image!(
"../../../assets/icons/notifications_dark_4x.png" "../../../assets/icons/notifications_dark_4x.png"
)) ))
} }
pub fn notifications_light_image() -> Image<'static> {
notifications_dark_image().tint(egui::Color32::BLACK)
}
pub fn repost_dark_image() -> Image<'static> { pub fn repost_dark_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/repost_icon_4x.png")) Image::new(include_image!("../../../assets/icons/repost_icon_4x.png"))
} }
@@ -208,10 +213,22 @@ pub fn media_upload_dark_image() -> Image<'static> {
)) ))
} }
pub fn wallet_image() -> Image<'static> { pub fn media_upload_light_image() -> Image<'static> {
media_upload_dark_image().tint(Color32::BLACK)
}
pub fn wallet_dark_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/wallet-icon.svg")) Image::new(include_image!("../../../assets/icons/wallet-icon.svg"))
} }
pub fn zap_image() -> Image<'static> { pub fn wallet_light_image() -> Image<'static> {
wallet_dark_image().tint(Color32::BLACK)
}
pub fn zap_dark_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/zap_4x.png")) Image::new(include_image!("../../../assets/icons/zap_4x.png"))
} }
pub fn zap_light_image() -> Image<'static> {
zap_dark_image().tint(Color32::BLACK)
}

View File

@@ -21,7 +21,7 @@ pub use note::{NoteContents, NoteOptions, NoteView};
pub use profile::{ProfilePic, ProfilePreview}; pub use profile::{ProfilePic, ProfilePreview};
pub use username::Username; pub use username::Username;
use egui::Margin; use egui::{Label, Margin, RichText};
/// This is kind of like the Widget trait but is meant for larger top-level /// This is kind of like the Widget trait but is meant for larger top-level
/// views that are typically stateful. /// views that are typically stateful.
@@ -58,3 +58,8 @@ pub fn hline_with_width(ui: &egui::Ui, range: egui::Rangef) {
let stroke = ui.style().visuals.widgets.noninteractive.bg_stroke; let stroke = ui.style().visuals.widgets.noninteractive.bg_stroke;
ui.painter().hline(range, resize_y, stroke); ui.painter().hline(range, resize_y, stroke);
} }
pub fn secondary_label(ui: &mut egui::Ui, s: impl Into<String>) {
let color = ui.style().visuals.noninteractive().fg_stroke.color;
ui.add(Label::new(RichText::new(s).size(10.0).color(color)).selectable(false));
}

View File

@@ -4,13 +4,14 @@ use crate::{
blur::imeta_blurhashes, blur::imeta_blurhashes,
jobs::JobsCache, jobs::JobsCache,
note::{NoteAction, NoteOptions, NoteResponse, NoteView}, note::{NoteAction, NoteOptions, NoteResponse, NoteView},
secondary_label,
}; };
use egui::{Color32, Hyperlink, RichText}; use egui::{Color32, Hyperlink, RichText};
use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
use tracing::warn; use tracing::warn;
use notedeck::{IsFollowing, NoteContext}; use notedeck::{IsFollowing, NoteCache, NoteContext};
use super::media::{find_renderable_media, image_carousel, RenderableMedia}; use super::media::{find_renderable_media, image_carousel, RenderableMedia};
@@ -53,11 +54,28 @@ impl egui::Widget for &mut NoteContents<'_, '_> {
self.options, self.options,
self.jobs, self.jobs,
); );
if self.options.contains(NoteOptions::ShowNoteClient) {
render_client(ui, self.note_context.note_cache, self.note);
}
self.action = result.action; self.action = result.action;
result.response result.response
} }
} }
#[profiling::function]
fn render_client(ui: &mut egui::Ui, note_cache: &mut NoteCache, note: &Note) {
let cached_note = note_cache.cached_note_or_insert_mut(note.key().unwrap(), note);
match cached_note.client.as_deref() {
Some(client) if !client.is_empty() => {
ui.horizontal(|ui| {
secondary_label(ui, format!("via {}", client));
});
}
_ => return,
}
}
/// Render an inline note preview with a border. These are used when /// Render an inline note preview with a border. These are used when
/// notes are references within a note /// notes are references within a note
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]

View File

@@ -4,8 +4,8 @@ pub mod media;
pub mod options; pub mod options;
pub mod reply_description; pub mod reply_description;
use crate::app_images;
use crate::jobs::JobsCache; use crate::jobs::JobsCache;
use crate::{app_images, secondary_label};
use crate::{ use crate::{
profile::name::one_line_display_name_widget, widgets::x_button, ProfilePic, ProfilePreview, profile::name::one_line_display_name_widget, widgets::x_button, ProfilePic, ProfilePreview,
PulseAlpha, Username, PulseAlpha, Username,
@@ -21,7 +21,7 @@ pub use options::NoteOptions;
pub use reply_description::reply_desc; pub use reply_description::reply_desc;
use egui::emath::{pos2, Vec2}; use egui::emath::{pos2, Vec2};
use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense}; use egui::{Id, Pos2, Rect, Response, RichText, Sense};
use enostr::{KeypairUnowned, NoteId, Pubkey}; use enostr::{KeypairUnowned, NoteId, Pubkey};
use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction}; use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
use notedeck::{ use notedeck::{
@@ -534,6 +534,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
cur_acc: cur_acc.keypair(), cur_acc: cur_acc.keypair(),
}) })
}; };
if self.options().contains(NoteOptions::ActionBar) { if self.options().contains(NoteOptions::ActionBar) {
note_action = render_note_actionbar( note_action = render_note_actionbar(
ui, ui,
@@ -828,11 +829,6 @@ fn render_note_actionbar(
}) })
} }
fn secondary_label(ui: &mut egui::Ui, s: impl Into<String>) {
let color = ui.style().visuals.noninteractive().fg_stroke.color;
ui.add(Label::new(RichText::new(s).size(10.0).color(color)));
}
#[profiling::function] #[profiling::function]
fn render_reltime( fn render_reltime(
ui: &mut egui::Ui, ui: &mut egui::Ui,
@@ -902,14 +898,14 @@ fn zap_button(state: AnyZapState, noteid: &[u8; 32]) -> impl egui::Widget + use<
move |ui: &mut egui::Ui| -> egui::Response { move |ui: &mut egui::Ui| -> egui::Response {
let (rect, size, resp) = crate::anim::hover_expand_small(ui, ui.id().with("zap")); let (rect, size, resp) = crate::anim::hover_expand_small(ui, ui.id().with("zap"));
let mut img = app_images::zap_image().max_width(size); let mut img = app_images::zap_dark_image().max_width(size);
let id = ui.id().with(("pulse", noteid)); let id = ui.id().with(("pulse", noteid));
let ctx = ui.ctx().clone(); let ctx = ui.ctx().clone();
match state { match state {
AnyZapState::None => { AnyZapState::None => {
if !ui.visuals().dark_mode { if !ui.visuals().dark_mode {
img = img.tint(egui::Color32::BLACK); img = app_images::zap_light_image();
} }
} }
AnyZapState::Pending => { AnyZapState::Pending => {

View File

@@ -22,6 +22,8 @@ bitflags! {
/// Is the content truncated? If the length is over a certain size it /// Is the content truncated? If the length is over a certain size it
/// will end with a ... and a "Show more" button. /// will end with a ... and a "Show more" button.
const Truncate = 1 << 11; const Truncate = 1 << 11;
/// Show note's client in the note header
const ShowNoteClient = 1 << 12;
} }
} }