From 8d9a5fd79fb1bff4d96ffb8683ed1ede3b4de6e8 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 11 Aug 2025 17:11:02 -0400 Subject: [PATCH] Try adding timezone awareness --- src/memories.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++----- src/state.rs | 3 +- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/memories.rs b/src/memories.rs index 1595de9..e6365f5 100644 --- a/src/memories.rs +++ b/src/memories.rs @@ -1,6 +1,6 @@ use actix_web::web::Data; use actix_web::{get, web, HttpRequest, HttpResponse, Responder}; -use chrono::{DateTime, Datelike, Local, NaiveDate, Utc}; +use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, TimeZone, Utc}; use opentelemetry::trace::{Span, Status, Tracer}; use opentelemetry::KeyValue; use serde::{Deserialize, Serialize}; @@ -23,6 +23,8 @@ pub enum MemoriesSpan { #[derive(Deserialize)] pub struct MemoriesRequest { pub span: Option, + /// Client timezone offset in minutes from UTC (e.g., -480 for PST, 60 for CET) + pub timezone_offset_minutes: Option, } #[derive(Debug, Serialize, Clone)] @@ -51,7 +53,24 @@ pub async fn list_memories( let span_mode = q.span.unwrap_or(MemoriesSpan::Day); let years_back: u32 = 15; - let now = Local::now().date_naive(); + // Create timezone from client offset, default to local timezone if not provided + let client_timezone = match q.timezone_offset_minutes { + Some(offset_mins) => { + let offset_secs = offset_mins * 60; + Some( + FixedOffset::east_opt(offset_secs) + .unwrap_or_else(|| FixedOffset::east_opt(0).unwrap()), + ) + } + None => None, + }; + + let now = if let Some(tz) = client_timezone { + Utc::now().with_timezone(&tz).date_naive() + } else { + Local::now().date_naive() + }; + let base = Path::new(&app_state.base_path); let mut memories_with_dates: Vec<(MemoryItem, NaiveDate)> = Vec::new(); @@ -68,7 +87,7 @@ pub async fn list_memories( } // Use created date if available, otherwise modified date for matching - let file_date = match file_best_local_date(path) { + let file_date = match file_best_date(path, &client_timezone) { Some(d) => d, None => continue, }; @@ -88,7 +107,8 @@ pub async fn list_memories( } } - memories_with_dates.sort_by_key(|k| k.1.ordinal()); + // Sort by day of the month + memories_with_dates.sort_by_key(|k| k.1.day()); let items: Vec = memories_with_dates.into_iter().map(|(m, _)| m).collect(); @@ -98,6 +118,13 @@ pub async fn list_memories( KeyValue::new("span", format!("{:?}", span_mode)), KeyValue::new("years_back", years_back.to_string()), KeyValue::new("result_count", items.len().to_string()), + KeyValue::new( + "client_timezone", + format!( + "{:?}", + client_timezone.unwrap_or_else(|| FixedOffset::east_opt(0).unwrap()) + ), + ), ], ); span.set_status(Status::Ok); @@ -105,11 +132,19 @@ pub async fn list_memories( HttpResponse::Ok().json(MemoriesResponse { items }) } -fn file_best_local_date(path: &Path) -> Option { +fn file_best_date(path: &Path, client_timezone: &Option) -> Option { let meta = std::fs::metadata(path).ok()?; let system_time = meta.created().ok().or_else(|| meta.modified().ok())?; - let local_dt = chrono::DateTime::::from(system_time); - Some(local_dt.date_naive()) + + let dt = if let Some(tz) = client_timezone { + let utc_dt: DateTime = system_time.into(); + utc_dt.with_timezone(tz).date_naive() + } else { + let local_dt = chrono::DateTime::::from(system_time); + local_dt.date_naive() + }; + + Some(dt) } fn file_times_epoch_secs(path: &Path) -> (Option, Option) { @@ -228,4 +263,30 @@ mod tests { assert!(!is_memories_match(file_date, today, MemoriesSpan::Day, 10)); assert!(is_memories_match(file_date, today, MemoriesSpan::Day, 20)); } + + #[test] + fn test_timezone_conversion() { + // Test file_best_date with different timezones + use std::fs::File; + use tempfile::tempdir; + + let temp_dir = tempdir().unwrap(); + let temp_file = temp_dir.path().join("test_file.jpg"); + File::create(&temp_file).unwrap(); + + // Test with PST (-8 hours = -480 minutes) + let pst_offset = FixedOffset::west_opt(8 * 3600).unwrap(); + let client_tz = Some(pst_offset); + + let date = file_best_date(&temp_file, &client_tz); + assert!(date.is_some()); + + // Test with no timezone (local) + let date_local = file_best_date(&temp_file, &None); + assert!(date_local.is_some()); + + // Both should return valid dates + assert!(date.unwrap().year() >= 2020); + assert!(date_local.unwrap().year() >= 2020); + } } diff --git a/src/state.rs b/src/state.rs index ab0f3c9..8594ec8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -75,6 +75,7 @@ impl AppState { #[cfg(test)] fn create_test_subdir(base_path: &std::path::Path, name: &str) -> std::path::PathBuf { let dir_path = base_path.join(name); - std::fs::create_dir_all(&dir_path).unwrap_or_else(|_| panic!("Failed to create {} directory", name)); + std::fs::create_dir_all(&dir_path) + .unwrap_or_else(|_| panic!("Failed to create {} directory", name)); dir_path }