notedeck

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

commit 0e65491ef17776df3142b37929357d609e45874e
parent 3f5036bd325b89010556fd7373dc86271b57d572
Author: Terry Yiu <git@tyiu.xyz>
Date:   Fri, 27 Jun 2025 00:16:48 -0400

Clean up time_ago_since, add tests, and internationalize strings

Changelog-Changed: Internationalized time ago strings
Signed-off-by: Terry Yiu <git@tyiu.xyz>

Diffstat:
Massets/translations/en-US/main.ftl | 24++++++++++++++++++++++++
Massets/translations/en-XA/main.ftl | 24++++++++++++++++++++++++
Mcrates/notedeck/src/time.rs | 327++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
3 files changed, 346 insertions(+), 29 deletions(-)

diff --git a/assets/translations/en-US/main.ftl b/assets/translations/en-US/main.ftl @@ -145,6 +145,27 @@ Copy_Pubkey_9cc4 = Copy Pubkey # Copy the text content of the note to clipboard Copy_Text_f81c = Copy Text +# Relative time in days +count_d_b9be = {$count}d + +# Relative time in hours +count_h_3ecb = {$count}h + +# Relative time in minutes +count_m_b41e = {$count}m + +# Relative time in months +count_mo_7aba = {$count}mo + +# Relative time in seconds +count_s_aa26 = {$count}s + +# Relative time in weeks +count_w_7468 = {$count}w + +# Relative time in years +count_y_9408 = {$count}y + # Button to create a new account Create_Account_6994 = Create Account @@ -346,6 +367,9 @@ Notifications_d673 = Notifications # Title for notifications column Notifications_ef56 = Notifications +# Relative time for very recent events (less than 3 seconds) +now_2181 = now + # Button label to open email client Open_Email_25e9 = Open Email diff --git a/assets/translations/en-XA/main.ftl b/assets/translations/en-XA/main.ftl @@ -145,6 +145,27 @@ Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"} # Copy the text content of the note to clipboard Copy_Text_f81c = {"["}Çópy Téxt{"]"} +# Relative time in days +count_d_b9be = {"["}{$count}d{"]"} + +# Relative time in hours +count_h_3ecb = {"["}{$count}h{"]"} + +# Relative time in minutes +count_m_b41e = {"["}{$count}m{"]"} + +# Relative time in months +count_mo_7aba = {"["}{$count}mó{"]"} + +# Relative time in seconds +count_s_aa26 = {"["}{$count}s{"]"} + +# Relative time in weeks +count_w_7468 = {"["}{$count}w{"]"} + +# Relative time in years +count_y_9408 = {"["}{$count}y{"]"} + # Button to create a new account Create_Account_6994 = {"["}Çréàté Àççóúñt{"]"} @@ -346,6 +367,9 @@ Notifications_d673 = {"["}Ñótífíçàtíóñs{"]"} # Title for notifications column Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"} +# Relative time for very recent events (less than 3 seconds) +now_2181 = {"["}ñów{"]"} + # Button label to open email client Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"} diff --git a/crates/notedeck/src/time.rs b/crates/notedeck/src/time.rs @@ -1,11 +1,24 @@ +use crate::tr; use std::time::{SystemTime, UNIX_EPOCH}; -pub fn time_ago_since(timestamp: u64) -> String { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); +// Time duration constants in seconds +const ONE_MINUTE_IN_SECONDS: u64 = 60; +const ONE_HOUR_IN_SECONDS: u64 = 3600; +const ONE_DAY_IN_SECONDS: u64 = 86_400; +const ONE_WEEK_IN_SECONDS: u64 = 604_800; +const ONE_MONTH_IN_SECONDS: u64 = 2_592_000; // 30 days +const ONE_YEAR_IN_SECONDS: u64 = 31_536_000; // 365 days + +// Range boundary constants for match patterns +const MAX_SECONDS: u64 = ONE_MINUTE_IN_SECONDS - 1; +const MAX_SECONDS_FOR_MINUTES: u64 = ONE_HOUR_IN_SECONDS - 1; +const MAX_SECONDS_FOR_HOURS: u64 = ONE_DAY_IN_SECONDS - 1; +const MAX_SECONDS_FOR_DAYS: u64 = ONE_WEEK_IN_SECONDS - 1; +const MAX_SECONDS_FOR_WEEKS: u64 = ONE_MONTH_IN_SECONDS - 1; +const MAX_SECONDS_FOR_MONTHS: u64 = ONE_YEAR_IN_SECONDS - 1; +/// Calculate relative time between two timestamps +fn time_ago_between(timestamp: u64, now: u64) -> String { // Determine if the timestamp is in the future or the past let duration = if now >= timestamp { now.saturating_sub(timestamp) @@ -13,43 +26,299 @@ pub fn time_ago_since(timestamp: u64) -> String { timestamp.saturating_sub(now) }; - let future = timestamp > now; - let relstr = if future { "+" } else { "" }; + let time_str = match duration { + 0..=2 => tr!( + "now", + "Relative time for very recent events (less than 3 seconds)" + ), + 3..=MAX_SECONDS => tr!("{count}s", "Relative time in seconds", count = duration), + ONE_MINUTE_IN_SECONDS..=MAX_SECONDS_FOR_MINUTES => tr!( + "{count}m", + "Relative time in minutes", + count = duration / ONE_MINUTE_IN_SECONDS + ), + ONE_HOUR_IN_SECONDS..=MAX_SECONDS_FOR_HOURS => tr!( + "{count}h", + "Relative time in hours", + count = duration / ONE_HOUR_IN_SECONDS + ), + ONE_DAY_IN_SECONDS..=MAX_SECONDS_FOR_DAYS => tr!( + "{count}d", + "Relative time in days", + count = duration / ONE_DAY_IN_SECONDS + ), + ONE_WEEK_IN_SECONDS..=MAX_SECONDS_FOR_WEEKS => tr!( + "{count}w", + "Relative time in weeks", + count = duration / ONE_WEEK_IN_SECONDS + ), + ONE_MONTH_IN_SECONDS..=MAX_SECONDS_FOR_MONTHS => tr!( + "{count}mo", + "Relative time in months", + count = duration / ONE_MONTH_IN_SECONDS + ), + _ => tr!( + "{count}y", + "Relative time in years", + count = duration / ONE_YEAR_IN_SECONDS + ), + }; - let years = duration / 31_536_000; // seconds in a year - if years >= 1 { - return format!("{relstr}{years}yr"); + if timestamp > now { + format!("+{}", time_str) + } else { + time_str } +} + +pub fn time_ago_since(timestamp: u64) -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + time_ago_between(timestamp, now) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; - let months = duration / 2_592_000; // seconds in a month (30.44 days) - if months >= 1 { - return format!("{relstr}{months}mth"); + fn get_current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() } - let weeks = duration / 604_800; // seconds in a week - if weeks >= 1 { - return format!("{relstr}{weeks}wk"); + #[test] + fn test_now_condition() { + let now = get_current_timestamp(); + + // Test 0 seconds ago + let result = time_ago_between(now, now); + assert_eq!( + result, "now", + "Expected 'now' for 0 seconds, got: {}", + result + ); + + // Test 1 second ago + let result = time_ago_between(now - 1, now); + assert_eq!( + result, "now", + "Expected 'now' for 1 second, got: {}", + result + ); + + // Test 2 seconds ago + let result = time_ago_between(now - 2, now); + assert_eq!( + result, "now", + "Expected 'now' for 2 seconds, got: {}", + result + ); } - let days = duration / 86_400; // seconds in a day - if days >= 1 { - return format!("{relstr}{days}d"); + #[test] + fn test_seconds_condition() { + let now = get_current_timestamp(); + + // Test 3 seconds ago + let result = time_ago_between(now - 3, now); + assert_eq!(result, "3s", "Expected '3s' for 3 seconds, got: {}", result); + + // Test 30 seconds ago + let result = time_ago_between(now - 30, now); + assert_eq!( + result, "30s", + "Expected '30s' for 30 seconds, got: {}", + result + ); + + // Test 59 seconds ago (max for seconds) + let result = time_ago_between(now - 59, now); + assert_eq!( + result, "59s", + "Expected '59s' for 59 seconds, got: {}", + result + ); } - let hours = duration / 3600; // seconds in an hour - if hours >= 1 { - return format!("{relstr}{hours}h"); + #[test] + fn test_minutes_condition() { + let now = get_current_timestamp(); + + // Test 1 minute ago + let result = time_ago_between(now - ONE_MINUTE_IN_SECONDS, now); + assert_eq!(result, "1m", "Expected '1m' for 1 minute, got: {}", result); + + // Test 30 minutes ago + let result = time_ago_between(now - 30 * ONE_MINUTE_IN_SECONDS, now); + assert_eq!( + result, "30m", + "Expected '30m' for 30 minutes, got: {}", + result + ); + + // Test 59 minutes ago (max for minutes) + let result = time_ago_between(now - 59 * ONE_MINUTE_IN_SECONDS, now); + assert_eq!( + result, "59m", + "Expected '59m' for 59 minutes, got: {}", + result + ); } - let minutes = duration / 60; // seconds in a minute - if minutes >= 1 { - return format!("{relstr}{minutes}m"); + #[test] + fn test_hours_condition() { + let now = get_current_timestamp(); + + // Test 1 hour ago + let result = time_ago_between(now - ONE_HOUR_IN_SECONDS, now); + assert_eq!(result, "1h", "Expected '1h' for 1 hour, got: {}", result); + + // Test 12 hours ago + let result = time_ago_between(now - 12 * ONE_HOUR_IN_SECONDS, now); + assert_eq!( + result, "12h", + "Expected '12h' for 12 hours, got: {}", + result + ); + + // Test 23 hours ago (max for hours) + let result = time_ago_between(now - 23 * ONE_HOUR_IN_SECONDS, now); + assert_eq!( + result, "23h", + "Expected '23h' for 23 hours, got: {}", + result + ); + } + + #[test] + fn test_days_condition() { + let now = get_current_timestamp(); + + // Test 1 day ago + let result = time_ago_between(now - ONE_DAY_IN_SECONDS, now); + assert_eq!(result, "1d", "Expected '1d' for 1 day, got: {}", result); + + // Test 3 days ago + let result = time_ago_between(now - 3 * ONE_DAY_IN_SECONDS, now); + assert_eq!(result, "3d", "Expected '3d' for 3 days, got: {}", result); + + // Test 6 days ago (max for days, before weeks) + let result = time_ago_between(now - 6 * ONE_DAY_IN_SECONDS, now); + assert_eq!(result, "6d", "Expected '6d' for 6 days, got: {}", result); } - let seconds = duration; - if seconds >= 3 { - return format!("{relstr}{seconds}s"); + #[test] + fn test_weeks_condition() { + let now = get_current_timestamp(); + + // Test 1 week ago + let result = time_ago_between(now - ONE_WEEK_IN_SECONDS, now); + assert_eq!(result, "1w", "Expected '1w' for 1 week, got: {}", result); + + // Test 4 weeks ago + let result = time_ago_between(now - 4 * ONE_WEEK_IN_SECONDS, now); + assert_eq!(result, "4w", "Expected '4w' for 4 weeks, got: {}", result); } - "now".to_string() + #[test] + fn test_months_condition() { + let now = get_current_timestamp(); + + // Test 1 month ago + let result = time_ago_between(now - ONE_MONTH_IN_SECONDS, now); + assert_eq!(result, "1mo", "Expected '1mo' for 1 month, got: {}", result); + + // Test 11 months ago (max for months, before years) + let result = time_ago_between(now - 11 * ONE_MONTH_IN_SECONDS, now); + assert_eq!( + result, "11mo", + "Expected '11mo' for 11 months, got: {}", + result + ); + } + + #[test] + fn test_years_condition() { + let now = get_current_timestamp(); + + // Test 1 year ago + let result = time_ago_between(now - ONE_YEAR_IN_SECONDS, now); + assert_eq!(result, "1y", "Expected '1y' for 1 year, got: {}", result); + + // Test 5 years ago + let result = time_ago_between(now - 5 * ONE_YEAR_IN_SECONDS, now); + assert_eq!(result, "5y", "Expected '5y' for 5 years, got: {}", result); + + // Test 10 years ago (reduced from 100 to avoid overflow) + let result = time_ago_between(now - 10 * ONE_YEAR_IN_SECONDS, now); + assert_eq!( + result, "10y", + "Expected '10y' for 10 years, got: {}", + result + ); + } + + #[test] + fn test_future_timestamps() { + let now = get_current_timestamp(); + + // Test 1 minute in the future + let result = time_ago_between(now + ONE_MINUTE_IN_SECONDS, now); + assert_eq!( + result, "+1m", + "Expected '+1m' for 1 minute in future, got: {}", + result + ); + + // Test 1 hour in the future + let result = time_ago_between(now + ONE_HOUR_IN_SECONDS, now); + assert_eq!( + result, "+1h", + "Expected '+1h' for 1 hour in future, got: {}", + result + ); + + // Test 1 day in the future + let result = time_ago_between(now + ONE_DAY_IN_SECONDS, now); + assert_eq!( + result, "+1d", + "Expected '+1d' for 1 day in future, got: {}", + result + ); + } + + #[test] + fn test_boundary_conditions() { + let now = get_current_timestamp(); + + // Test boundary between seconds and minutes + let result = time_ago_between(now - 60, now); + assert_eq!( + result, "1m", + "Expected '1m' for exactly 60 seconds, got: {}", + result + ); + + // Test boundary between minutes and hours + let result = time_ago_between(now - 3600, now); + assert_eq!( + result, "1h", + "Expected '1h' for exactly 3600 seconds, got: {}", + result + ); + + // Test boundary between hours and days + let result = time_ago_between(now - 86400, now); + assert_eq!( + result, "1d", + "Expected '1d' for exactly 86400 seconds, got: {}", + result + ); + } }