goblin: refine the activity row and drop list seconds
Rebuild the activity row so the message pins left and truncates while the signed amount pins right with the date/time right-aligned directly beneath it. List timestamps lose the seconds (HH:MM); the tap-in detail view keeps the full second-precision time. A row with no timestamp (a canceled tx) draws no right-side time slot at all and folds its status word into the left message instead. Shared note/time derivation lives in one helper.
This commit is contained in:
+39
-30
@@ -1775,24 +1775,12 @@ impl GoblinWalletView {
|
||||
};
|
||||
let amount =
|
||||
format!("{}{}{}", sign, w::amount_str(item.amount), w::TSU);
|
||||
let status_word = if item.canceled {
|
||||
t!("goblin.activity.canceled").to_string()
|
||||
} else {
|
||||
t!("goblin.activity.pending").to_string()
|
||||
};
|
||||
// Note truncates; the tail (date/time when
|
||||
// confirmed, else the status word) stays in full.
|
||||
let note = item.note.as_deref().unwrap_or("");
|
||||
let tail = if item.confirmed {
|
||||
View::format_time(item.time)
|
||||
} else {
|
||||
status_word.clone()
|
||||
};
|
||||
let (note, time) = Self::activity_note_time(item);
|
||||
if w::activity_row(
|
||||
ui,
|
||||
&item.title,
|
||||
note,
|
||||
&tail,
|
||||
¬e,
|
||||
&time,
|
||||
item.npub.as_deref().unwrap_or(""),
|
||||
&amount,
|
||||
item.incoming,
|
||||
@@ -1864,6 +1852,39 @@ impl GoblinWalletView {
|
||||
close
|
||||
}
|
||||
|
||||
/// List-row timestamp: date + HH:MM, no seconds. The tap-in detail view keeps
|
||||
/// the full timestamp to the second (see [`View::format_time`]).
|
||||
fn list_time(ts: i64) -> String {
|
||||
let utc_offset = chrono::Local::now().offset().local_minus_utc();
|
||||
chrono::DateTime::from_timestamp(ts + utc_offset as i64, 0)
|
||||
.map(|t| t.format("%d/%m/%Y %H:%M").to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The (left message, right timestamp) an [`ActivityItem`] shows in a row. The
|
||||
/// timestamp (no seconds) is only set for a confirmed tx; otherwise the status
|
||||
/// word (canceled/pending) folds into the message so a row with no time still
|
||||
/// reads its state without an empty right-side time slot.
|
||||
fn activity_note_time(item: &ActivityItem) -> (String, String) {
|
||||
let status_word = if item.canceled {
|
||||
t!("goblin.activity.canceled").to_string()
|
||||
} else {
|
||||
t!("goblin.activity.pending").to_string()
|
||||
};
|
||||
let time = if item.confirmed {
|
||||
Self::list_time(item.time)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let note = match (item.note.as_deref(), item.confirmed) {
|
||||
(Some(n), false) => format!("{n} · {status_word}"),
|
||||
(None, false) => status_word,
|
||||
(Some(n), true) => n.to_string(),
|
||||
(None, true) => String::new(),
|
||||
};
|
||||
(note, time)
|
||||
}
|
||||
|
||||
/// Friendly day-grouping label for the activity feed.
|
||||
fn day_label(ts: i64) -> String {
|
||||
use chrono::{TimeZone, Utc};
|
||||
@@ -1982,25 +2003,13 @@ impl GoblinWalletView {
|
||||
"− "
|
||||
};
|
||||
let amount = format!("{}{}{}", sign, w::amount_str(item.amount), w::TSU);
|
||||
let status_word = if item.canceled {
|
||||
t!("goblin.activity.canceled").to_string()
|
||||
} else {
|
||||
t!("goblin.activity.pending").to_string()
|
||||
};
|
||||
// Note truncates; the tail (date/time when confirmed, else the status
|
||||
// word) is pinned right and kept in full.
|
||||
let note = item.note.as_deref().unwrap_or("");
|
||||
let tail = if item.confirmed {
|
||||
View::format_time(item.time)
|
||||
} else {
|
||||
status_word.clone()
|
||||
};
|
||||
let (note, time) = Self::activity_note_time(item);
|
||||
let tex = self.handle_tex(ui.ctx(), wallet, &item.title);
|
||||
if w::activity_row(
|
||||
ui,
|
||||
&item.title,
|
||||
note,
|
||||
&tail,
|
||||
¬e,
|
||||
&time,
|
||||
item.npub.as_deref().unwrap_or(""),
|
||||
&amount,
|
||||
item.incoming,
|
||||
|
||||
@@ -615,13 +615,15 @@ pub fn balance_hero(
|
||||
}
|
||||
}
|
||||
|
||||
/// An activity row: avatar, title, subtitle, signed amount.
|
||||
/// An activity row: avatar, a left title/message column that truncates, and a
|
||||
/// right column with the signed amount over the date/time. `time` is the
|
||||
/// right-side timestamp (empty draws no time line — e.g. a canceled tx).
|
||||
/// Returns the row click response.
|
||||
pub fn activity_row(
|
||||
ui: &mut Ui,
|
||||
title: &str,
|
||||
note: &str,
|
||||
tail: &str,
|
||||
time: &str,
|
||||
id: &str,
|
||||
amount: &str,
|
||||
incoming: bool,
|
||||
@@ -662,29 +664,56 @@ pub fn activity_row(
|
||||
avatar_any(ui, title, id, 40.0, tex);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
// Reserve the amount as its own right-hand column FIRST. Placing it
|
||||
// before the text means the title/subtitle column is bounded to the
|
||||
// remaining width and truncates cleanly, instead of stretching under
|
||||
// the amount and colliding with it. Centered against the whole stack,
|
||||
// the amount lands between the title and subtitle lines.
|
||||
// Right column FIRST so the left title/message column is bounded to the
|
||||
// remaining width and truncates cleanly. The amount sits on top with the
|
||||
// date/time right-aligned directly beneath it; a row with no timestamp
|
||||
// (a canceled tx) draws no time line at all.
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if !amount.is_empty() {
|
||||
// A canceled tx delivered no funds: mute the amount so it never
|
||||
// reads as a completed green credit (or a real debit).
|
||||
ui.label(
|
||||
RichText::new(amount)
|
||||
.font(FontId::new(15.0, fonts::mono_semibold()))
|
||||
.color(if canceled {
|
||||
t.text_dim
|
||||
} else if incoming {
|
||||
t.pos
|
||||
} else {
|
||||
t.text
|
||||
}),
|
||||
let amt_ink = if canceled {
|
||||
t.text_dim
|
||||
} else if incoming {
|
||||
t.pos
|
||||
} else {
|
||||
t.text
|
||||
};
|
||||
let amt_font = FontId::new(15.0, fonts::mono_semibold());
|
||||
let time_font = FontId::new(13.0, fonts::regular());
|
||||
let amt_g = (!amount.is_empty()).then(|| {
|
||||
ui.painter()
|
||||
.layout_no_wrap(amount.to_string(), amt_font, amt_ink)
|
||||
});
|
||||
let time_g = (!time.is_empty()).then(|| {
|
||||
ui.painter()
|
||||
.layout_no_wrap(time.to_string(), time_font, t.text_dim)
|
||||
});
|
||||
let col_w = amt_g
|
||||
.as_ref()
|
||||
.map(|g| g.size().x)
|
||||
.unwrap_or(0.0)
|
||||
.max(time_g.as_ref().map(|g| g.size().x).unwrap_or(0.0));
|
||||
if col_w > 0.0 {
|
||||
let amt_h = amt_g.as_ref().map(|g| g.size().y).unwrap_or(0.0);
|
||||
let time_h = time_g.as_ref().map(|g| g.size().y).unwrap_or(0.0);
|
||||
let col_h = amt_h + if time_h > 0.0 { 2.0 + time_h } else { 0.0 };
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2::new(col_w, col_h),
|
||||
Layout::top_down(Align::Max),
|
||||
|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
if let Some(g) = amt_g {
|
||||
let (r, _) = ui.allocate_exact_size(g.size(), Sense::hover());
|
||||
ui.painter().galley(r.min, g, amt_ink);
|
||||
}
|
||||
if let Some(g) = time_g {
|
||||
let (r, _) = ui.allocate_exact_size(g.size(), Sense::hover());
|
||||
ui.painter().galley(r.min, g, t.text_dim);
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
// Remaining width to the left holds the title + subtitle stack.
|
||||
// Remaining width to the left: the counterparty/title on top, the
|
||||
// message pinned left and truncated with an ellipsis beneath it.
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.add(
|
||||
@@ -695,7 +724,16 @@ pub fn activity_row(
|
||||
)
|
||||
.truncate(),
|
||||
);
|
||||
activity_subtitle(ui, note, tail, t.text_dim);
|
||||
if !note.is_empty() {
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
RichText::new(note)
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
)
|
||||
.truncate(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -706,40 +744,6 @@ pub fn activity_row(
|
||||
resp
|
||||
}
|
||||
|
||||
/// Single-line subtitle for an activity row.
|
||||
///
|
||||
/// The `tail` (date/time, or a short status word) is pinned to the right and
|
||||
/// never clipped; the `note` takes whatever width is left and gets the
|
||||
/// ellipsis when it runs out of room. When only one part is present it is
|
||||
/// left-aligned under the title, truncating on its own (e.g. a long npub in
|
||||
/// the contact picker).
|
||||
fn activity_subtitle(ui: &mut Ui, note: &str, tail: &str, color: Color32) {
|
||||
let dim = |s: &str| {
|
||||
RichText::new(s.to_string())
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(color)
|
||||
};
|
||||
match (note.is_empty(), tail.is_empty()) {
|
||||
(true, true) => {}
|
||||
(false, true) => {
|
||||
ui.add(egui::Label::new(dim(note)).truncate());
|
||||
}
|
||||
(true, false) => {
|
||||
ui.add(egui::Label::new(dim(tail)).truncate());
|
||||
}
|
||||
(false, false) => {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
// Rightmost, full: the date/time (with seconds) is never clipped.
|
||||
ui.add(egui::Label::new(dim(tail)));
|
||||
ui.add(egui::Label::new(dim("·")));
|
||||
// Leftmost, fills the gap: the note truncates with an ellipsis.
|
||||
ui.add(egui::Label::new(dim(note)).truncate());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Section header used above grouped lists.
|
||||
pub fn section_header(ui: &mut Ui, text: &str) {
|
||||
ui.add_space(8.0);
|
||||
|
||||
Reference in New Issue
Block a user