notedeck

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

commit 64ac06791af36fd36327be479cbccfee1aad148c
parent a6a89307f13d3d69f1f5b46701fbbd6c1260b842
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 16 Jul 2025 14:00:14 -0700

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

Diffstat:
Mcrates/notedeck/src/app.rs | 9++-------
Mcrates/notedeck/src/args.rs | 4++++
Mcrates/notedeck/src/note/mod.rs | 16++++++++++++++++
Mcrates/notedeck/src/notecache.rs | 14++++++++++++--
Mcrates/notedeck/src/zaps/zap.rs | 17-----------------
Mcrates/notedeck_chrome/src/chrome.rs | 14+++++++-------
Mcrates/notedeck_columns/src/app.rs | 4++++
Mcrates/notedeck_columns/src/args.rs | 3+++
Mcrates/notedeck_columns/src/ui/note/post.rs | 8+++++---
Mcrates/notedeck_columns/src/ui/profile/mod.rs | 4++--
Mcrates/notedeck_ui/src/app_images.rs | 39++++++++++++++++++++++++++++-----------
Mcrates/notedeck_ui/src/lib.rs | 7++++++-
Mcrates/notedeck_ui/src/note/contents.rs | 20+++++++++++++++++++-
Mcrates/notedeck_ui/src/note/mod.rs | 14+++++---------
Mcrates/notedeck_ui/src/note/options.rs | 2++
15 files changed, 115 insertions(+), 60 deletions(-)

diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs @@ -8,6 +8,7 @@ use crate::{ DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler, UnknownIds, }; +use egui::Margin; use egui::ThemePreference; use egui_winit::clipboard::Clipboard; use enostr::RelayPool; @@ -51,14 +52,8 @@ pub struct Notedeck { /// Our chrome, which is basically nothing 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 { - inner_margin, + inner_margin: Margin::ZERO, fill: style.visuals.panel_fill, ..Default::default() }) diff --git a/crates/notedeck/src/args.rs b/crates/notedeck/src/args.rs @@ -6,6 +6,7 @@ use tracing::error; pub struct Args { pub relays: Vec<String>, pub is_mobile: Option<bool>, + pub show_note_client: bool, pub keys: Vec<Keypair>, pub light: bool, pub debug: bool, @@ -28,6 +29,7 @@ impl Args { is_mobile: None, keys: vec![], light: false, + show_note_client: false, debug: false, relay_debug: false, tests: false, @@ -116,6 +118,8 @@ impl Args { res.use_keystore = false; } else if arg == "--relay-debug" { res.relay_debug = true; + } else if arg == "--show-note-client" { + res.show_note_client = true; } else { unrecognized_args.insert(arg.clone()); } diff --git a/crates/notedeck/src/note/mod.rs b/crates/notedeck/src/note/mod.rs @@ -193,3 +193,19 @@ where |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) + }) +} diff --git a/crates/notedeck/src/notecache.rs b/crates/notedeck/src/notecache.rs @@ -33,18 +33,28 @@ impl NoteCache { #[derive(Clone)] pub struct CachedNote { reltime: TimeCached<String>, + pub client: Option<String>, pub reply: NoteReplyBuf, } 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 reltime = TimeCached::new( Duration::from_secs(1), Box::new(move || time_ago_since(created_at)), ); 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 { diff --git a/crates/notedeck/src/zaps/zap.rs b/crates/notedeck/src/zaps/zap.rs @@ -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> { if let Some(note_zapped) = tags.note_zapped { Some(ZapTarget::Note(NoteZapTarget { diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs @@ -9,7 +9,6 @@ use notedeck::{App, AppAction, AppContext, NotedeckTextStyle, UserAccount, Walle use notedeck_columns::{ column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus, }; - use notedeck_dave::{Dave, DaveAvatar}; use notedeck_ui::{app_images, AnimationHelper, ProfilePic}; @@ -289,7 +288,7 @@ impl Chrome { /// How far is the chrome panel expanded? fn amount_open(&self, ui: &mut egui::Ui) -> f32 { 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 } @@ -406,7 +405,7 @@ impl Chrome { // macos needs a bit of space to make room for window // minimize/close buttons if cfg!(target_os = "macos") { - ui.add_space(28.0); + ui.add_space(30.0); } else { // we still want *some* padding so that it aligns with the + button regardless 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 mut img = app_images::wallet_image().max_width(img_size); - - if !ui.visuals().dark_mode { - img = img.tint(egui::Color32::BLACK); + let img = if !ui.visuals().dark_mode { + app_images::wallet_light_image() + } else { + app_images::wallet_dark_image() } + .max_width(img_size); let helper = AnimationHelper::new(ui, "wallet-icon", vec2(max_size, max_size)); diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -452,6 +452,10 @@ impl Damus { NoteOptions::HideMedia, 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::SinceOptimize, diff --git a/crates/notedeck_columns/src/args.rs b/crates/notedeck_columns/src/args.rs @@ -11,6 +11,7 @@ pub enum ColumnsFlag { Textmode, Scramble, NoMedia, + ShowNoteClient, } pub struct ColumnsArgs { @@ -52,6 +53,8 @@ impl ColumnsArgs { res.clear_flag(ColumnsFlag::SinceOptimize); } else if arg == "--scramble" { res.set_flag(ColumnsFlag::Scramble); + } else if arg == "--show-note-client" { + res.set_flag(ColumnsFlag::ShowNoteClient); } else if arg == "--no-media" { res.set_flag(ColumnsFlag::NoMedia); } else if arg == "--filter" { diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs @@ -643,15 +643,17 @@ fn media_upload_button() -> impl egui::Widget { painter.rect_filled(resp.rect, 8.0, fill_color); 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 { - upload_img = upload_img.tint(egui::Color32::BLACK); + let upload_img = if ui.visuals().dark_mode { + app_images::media_upload_dark_image() + } else { + app_images::media_upload_light_image() }; upload_img .max_size(egui::vec2(16.0, 16.0)) .paint_at(ui, resp.rect.shrink(8.0)); + resp } } diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -261,9 +261,9 @@ enum ProfileType { fn handle_link(ui: &mut egui::Ui, website_url: &str) { let img = if ui.visuals().dark_mode { - app_images::link_image() + app_images::link_dark_image() } else { - app_images::link_image().tint(egui::Color32::BLACK) + app_images::link_light_image() }; ui.add(img); diff --git a/crates/notedeck_ui/src/app_images.rs b/crates/notedeck_ui/src/app_images.rs @@ -1,5 +1,5 @@ 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 { 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> { - Image::new(include_image!("../../../assets/icons/home-toolbar.png")) +pub fn home_light_image() -> Image<'static> { + home_dark_image().tint(Color32::BLACK) } -pub fn home_light_image() -> Image<'static> { - home_dark_image().tint(egui::Color32::BLACK) +pub fn home_dark_image() -> Image<'static> { + Image::new(include_image!("../../../assets/icons/home-toolbar.png")) } 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")) } -pub fn link_image() -> Image<'static> { +pub fn link_dark_image() -> Image<'static> { 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> { 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> { Image::new(include_image!( "../../../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> { 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")) } -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")) } + +pub fn zap_light_image() -> Image<'static> { + zap_dark_image().tint(Color32::BLACK) +} diff --git a/crates/notedeck_ui/src/lib.rs b/crates/notedeck_ui/src/lib.rs @@ -21,7 +21,7 @@ pub use note::{NoteContents, NoteOptions, NoteView}; pub use profile::{ProfilePic, ProfilePreview}; 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 /// 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; 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)); +} diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs @@ -4,13 +4,14 @@ use crate::{ blur::imeta_blurhashes, jobs::JobsCache, note::{NoteAction, NoteOptions, NoteResponse, NoteView}, + secondary_label, }; use egui::{Color32, Hyperlink, RichText}; use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use tracing::warn; -use notedeck::{IsFollowing, NoteContext}; +use notedeck::{IsFollowing, NoteCache, NoteContext}; use super::media::{find_renderable_media, image_carousel, RenderableMedia}; @@ -53,11 +54,28 @@ impl egui::Widget for &mut NoteContents<'_, '_> { self.options, self.jobs, ); + if self.options.contains(NoteOptions::ShowNoteClient) { + render_client(ui, self.note_context.note_cache, self.note); + } self.action = result.action; 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 /// notes are references within a note #[allow(clippy::too_many_arguments)] diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs @@ -4,8 +4,8 @@ pub mod media; pub mod options; pub mod reply_description; -use crate::app_images; use crate::jobs::JobsCache; +use crate::{app_images, secondary_label}; use crate::{ profile::name::one_line_display_name_widget, widgets::x_button, ProfilePic, ProfilePreview, PulseAlpha, Username, @@ -21,7 +21,7 @@ pub use options::NoteOptions; pub use reply_description::reply_desc; 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 nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction}; use notedeck::{ @@ -534,6 +534,7 @@ impl<'a, 'd> NoteView<'a, 'd> { cur_acc: cur_acc.keypair(), }) }; + if self.options().contains(NoteOptions::ActionBar) { note_action = render_note_actionbar( 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] fn render_reltime( 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 { 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 ctx = ui.ctx().clone(); match state { AnyZapState::None => { if !ui.visuals().dark_mode { - img = img.tint(egui::Color32::BLACK); + img = app_images::zap_light_image(); } } AnyZapState::Pending => { diff --git a/crates/notedeck_ui/src/note/options.rs b/crates/notedeck_ui/src/note/options.rs @@ -22,6 +22,8 @@ bitflags! { /// Is the content truncated? If the length is over a certain size it /// will end with a ... and a "Show more" button. const Truncate = 1 << 11; + /// Show note's client in the note header + const ShowNoteClient = 1 << 12; } }