Merge remote-tracking branch 'github/pr/1025'

This commit is contained in:
William Casarin
2025-08-03 10:38:38 -07:00
12 changed files with 77 additions and 27 deletions

1
Cargo.lock generated
View File

@@ -3485,6 +3485,7 @@ dependencies = [
"bincode", "bincode",
"bitflags 2.9.1", "bitflags 2.9.1",
"blurhash", "blurhash",
"chrono",
"dirs", "dirs",
"eframe", "eframe",
"egui", "egui",

View File

@@ -14,6 +14,7 @@ members = [
[workspace.dependencies] [workspace.dependencies]
opener = "0.8.2" opener = "0.8.2"
chrono = "0.4.40"
base32 = "0.4.0" base32 = "0.4.0"
base64 = "0.22.1" base64 = "0.22.1"
rmpv = "1.3.0" rmpv = "1.3.0"

View File

@@ -49,6 +49,7 @@ once_cell = { workspace = true }
md5 = { workspace = true } md5 = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
regex = "1" regex = "1"
chrono = { workspace = true }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }

View File

@@ -80,6 +80,7 @@ pub use storage::{AccountStorage, DataPath, DataPathType, Directory};
pub use style::NotedeckTextStyle; pub use style::NotedeckTextStyle;
pub use theme::ColorTheme; pub use theme::ColorTheme;
pub use time::time_ago_since; pub use time::time_ago_since;
pub use time::time_format;
pub use timecache::TimeCached; pub use timecache::TimeCached;
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds}; pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes}; pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};

View File

@@ -1,4 +1,5 @@
use crate::{tr, Localization}; use crate::{tr, Localization};
use chrono::DateTime;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
// Time duration constants in seconds // Time duration constants in seconds
@@ -83,6 +84,14 @@ fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String
} }
} }
pub fn time_format(_i18n: &mut Localization, timestamp: u64) -> String {
// TODO: format this using the selected locale
DateTime::from_timestamp(timestamp as i64, 0)
.unwrap()
.format("%l:%M %p %b %d, %Y")
.to_string()
}
pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String { pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
let now = SystemTime::now() let now = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)

View File

@@ -591,6 +591,7 @@ fn render_nav_body(
) )
.ui(ui) .ui(ui)
.map(RenderNavAction::SettingsAction), .map(RenderNavAction::SettingsAction),
Route::Reply(id) => { Route::Reply(id) => {
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) { let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
txn txn

View File

@@ -270,6 +270,7 @@ impl<'a> SettingsView<'a> {
}); });
let txn = Transaction::new(self.note_context.ndb).unwrap(); let txn = Transaction::new(self.note_context.ndb).unwrap();
if let Some(note_id) = NoteId::from_bech(PREVIEW_NOTE_ID) { if let Some(note_id) = NoteId::from_bech(PREVIEW_NOTE_ID) {
if let Ok(preview_note) = if let Ok(preview_note) =
self.note_context.ndb.get_note_by_id(&txn, note_id.bytes()) self.note_context.ndb.get_note_by_id(&txn, note_id.bytes())
@@ -277,7 +278,6 @@ impl<'a> SettingsView<'a> {
notedeck_ui::padding(8.0, ui, |ui| { notedeck_ui::padding(8.0, ui, |ui| {
if is_narrow(ui.ctx()) { if is_narrow(ui.ctx()) {
ui.set_max_width(ui.available_width()); ui.set_max_width(ui.available_width());
}
NoteView::new( NoteView::new(
self.note_context, self.note_context,
@@ -288,6 +288,7 @@ impl<'a> SettingsView<'a> {
.actionbar(false) .actionbar(false)
.options_button(false) .options_button(false)
.show(ui); .show(ui);
}
}); });
ui.separator(); ui.separator();
} }

View File

@@ -292,6 +292,7 @@ struct ThreadNote<'a> {
impl<'a> ThreadNote<'a> { impl<'a> ThreadNote<'a> {
fn options(&self, mut cur_options: NoteOptions) -> NoteOptions { fn options(&self, mut cur_options: NoteOptions) -> NoteOptions {
cur_options.set(NoteOptions::ShowCreatedAtBottom, true);
match self.note_type { match self.note_type {
ThreadNoteType::Chain { root: _ } => cur_options, ThreadNoteType::Chain { root: _ } => cur_options,
ThreadNoteType::Selected { root: _ } => { ThreadNoteType::Selected { root: _ } => {

View File

@@ -18,7 +18,7 @@ serde_json = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
nostrdb = { workspace = true } nostrdb = { workspace = true }
hex = { workspace = true } hex = { workspace = true }
chrono = "0.4.40" chrono = { workspace = true }
rand = "0.9.0" rand = "0.9.0"
bytemuck = "1.22.0" bytemuck = "1.22.0"
futures = "0.3.31" futures = "0.3.31"

View File

@@ -1,16 +1,16 @@
use super::media::image_carousel;
use crate::{ use crate::{
note::{NoteAction, NoteOptions, NoteResponse, NoteView}, note::{NoteAction, NoteOptions, NoteResponse, NoteView},
secondary_label, secondary_label,
}; };
use notedeck::{JobsCache, RenderableMedia};
use egui::{Color32, Hyperlink, Label, RichText}; use egui::{Color32, Hyperlink, Label, RichText};
use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
use notedeck::{
time_format, update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle,
};
use notedeck::{JobsCache, RenderableMedia};
use tracing::warn; use tracing::warn;
use super::media::image_carousel;
use notedeck::{update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle};
pub struct NoteContents<'a, 'd> { pub struct NoteContents<'a, 'd> {
note_context: &'a mut NoteContext<'d>, note_context: &'a mut NoteContext<'d>,
txn: &'a Transaction, txn: &'a Transaction,
@@ -42,8 +42,13 @@ impl<'a, 'd> NoteContents<'a, 'd> {
impl egui::Widget for &mut NoteContents<'_, '_> { impl egui::Widget for &mut NoteContents<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response { fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let create_at_bottom = self.options.contains(NoteOptions::ShowCreatedAtBottom);
if self.options.contains(NoteOptions::ShowNoteClientTop) { if self.options.contains(NoteOptions::ShowNoteClientTop) {
render_client(ui, self.note_context.note_cache, self.note); render_client(ui, self.note_context.note_cache, self.note, false);
}
// bottom created at only on selected note
if create_at_bottom {
self.options.set(NoteOptions::ShowCreatedAtBottom, false);
} }
let result = render_note_contents( let result = render_note_contents(
ui, ui,
@@ -53,21 +58,39 @@ impl egui::Widget for &mut NoteContents<'_, '_> {
self.options, self.options,
self.jobs, self.jobs,
); );
if self.options.contains(NoteOptions::ShowNoteClientBottom) { ui.horizontal(|ui| {
render_client(ui, self.note_context.note_cache, self.note); if create_at_bottom {
secondary_label(
ui,
time_format(self.note_context.i18n, self.note.created_at()),
);
} }
if self.options.contains(NoteOptions::ShowNoteClientBottom) {
render_client(
ui,
self.note_context.note_cache,
self.note,
create_at_bottom,
);
}
});
self.action = result.action; self.action = result.action;
result.response result.response
} }
} }
#[profiling::function] #[profiling::function]
fn render_client(ui: &mut egui::Ui, note_cache: &mut NoteCache, note: &Note) { fn render_client(ui: &mut egui::Ui, note_cache: &mut NoteCache, note: &Note, before: bool) {
let cached_note = note_cache.cached_note_or_insert_mut(note.key().unwrap(), note); let cached_note = note_cache.cached_note_or_insert_mut(note.key().unwrap(), note);
match cached_note.client.as_deref() { match cached_note.client.as_deref() {
Some(client) if !client.is_empty() => { Some(client) if !client.is_empty() => {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if before {
secondary_label(ui, "");
}
secondary_label(ui, format!("via {client}")); secondary_label(ui, format!("via {client}"));
}); });
} }

View File

@@ -212,7 +212,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0)); let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0));
ui.allocate_rect(rect, Sense::hover()); ui.allocate_rect(rect, Sense::hover());
ui.put(rect, |ui: &mut egui::Ui| { ui.put(rect, |ui: &mut egui::Ui| {
render_reltime(ui, self.note_context.i18n, self.note.created_at(), false).response render_notetime(ui, self.note_context.i18n, self.note.created_at(), false).response
}); });
let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0)); let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0));
ui.allocate_rect(rect, Sense::hover()); ui.allocate_rect(rect, Sense::hover());
@@ -363,13 +363,17 @@ impl<'a, 'd> NoteView<'a, 'd> {
note: &Note, note: &Note,
profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>, profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
show_unread_indicator: bool, show_unread_indicator: bool,
flags: NoteOptions,
) { ) {
let horiz_resp = ui let horiz_resp = ui
.horizontal(|ui| { .horizontal(|ui| {
ui.spacing_mut().item_spacing.x = if is_narrow(ui.ctx()) { 1.0 } else { 2.0 }; ui.spacing_mut().item_spacing.x = if is_narrow(ui.ctx()) { 1.0 } else { 2.0 };
ui.add(Username::new(i18n, profile.as_ref().ok(), note.pubkey()).abbreviated(20)); let response = ui
.add(Username::new(i18n, profile.as_ref().ok(), note.pubkey()).abbreviated(20));
render_reltime(ui, i18n, note.created_at(), true); if !flags.contains(NoteOptions::ShowCreatedAtBottom) {
return render_notetime(ui, i18n, note.created_at(), true).response;
}
response
}) })
.response; .response;
@@ -417,6 +421,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
self.note, self.note,
profile, profile,
self.show_unread_indicator, self.show_unread_indicator,
self.flags,
); );
}) })
.response .response
@@ -503,6 +508,8 @@ impl<'a, 'd> NoteView<'a, 'd> {
let pfp_rect = pfp_resp.bounding_rect; let pfp_rect = pfp_resp.bounding_rect;
let mut note_action: Option<NoteAction> = pfp_resp.into_action(self.note.pubkey()); let mut note_action: Option<NoteAction> = pfp_resp.into_action(self.note.pubkey());
self.flags.set(NoteOptions::ShowCreatedAtBottom, false);
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
NoteView::note_header( NoteView::note_header(
ui, ui,
@@ -510,6 +517,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
self.note, self.note,
profile, profile,
self.show_unread_indicator, self.show_unread_indicator,
self.flags,
); );
ui.horizontal_wrapped(|ui| 's: { ui.horizontal_wrapped(|ui| 's: {
@@ -862,7 +870,7 @@ fn render_note_actionbar(
} }
#[profiling::function] #[profiling::function]
fn render_reltime( fn render_notetime(
ui: &mut egui::Ui, ui: &mut egui::Ui,
i18n: &mut Localization, i18n: &mut Localization,
created_at: u64, created_at: u64,

View File

@@ -22,11 +22,14 @@ 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 /// Show note's client in the note content
const ShowNoteClientTop = 1 << 12; const ShowNoteClientTop = 1 << 12;
const ShowNoteClientBottom = 1 << 13; const ShowNoteClientBottom = 1 << 13;
const RepliesNewestFirst = 1 << 14; const RepliesNewestFirst = 1 << 14;
// Show note's created at note bottom
const ShowCreatedAtBottom = 1 << 15;
} }
} }