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 LoĢpez Guevara (2):
refactor: use Margin:ZERO
feat(note-view): show note client
Diffstat:
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;
}
}