refactor: switch from chrono to jiff
This commit is contained in:
parent
79a054ab40
commit
3ffdf8f0d7
12 changed files with 205 additions and 161 deletions
57
Cargo.lock
generated
57
Cargo.lock
generated
|
|
@ -1280,6 +1280,47 @@ version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff"
|
||||||
|
version = "0.2.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-static",
|
||||||
|
"jiff-tzdb-platform",
|
||||||
|
"log",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"serde_core",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-static"
|
||||||
|
version = "0.2.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-tzdb"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-tzdb-platform"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-tzdb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.81"
|
version = "0.3.81"
|
||||||
|
|
@ -1427,6 +1468,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"icalendar",
|
"icalendar",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
"jiff",
|
||||||
"listenfd",
|
"listenfd",
|
||||||
"markdown",
|
"markdown",
|
||||||
"maud",
|
"maud",
|
||||||
|
|
@ -1847,6 +1889,21 @@ version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ clap = { version = "4.5.53", features = ["derive"] }
|
||||||
http = "1.3.1"
|
http = "1.3.1"
|
||||||
icalendar = "0.17.5"
|
icalendar = "0.17.5"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
|
jiff = { version = "0.2.23", features = ["serde"] }
|
||||||
listenfd = "1.0.2"
|
listenfd = "1.0.2"
|
||||||
markdown = "1.0.0"
|
markdown = "1.0.0"
|
||||||
maud = { version = "0.27.0", features = ["axum"] }
|
maud = { version = "0.27.0", features = ["axum"] }
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ test.skip("groups wrap nicely", async ({ page }) => {
|
||||||
// that the text is all on one line. Manual inspection looks good at time of writing.
|
// that the text is all on one line. Manual inspection looks good at time of writing.
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allow marking as hidden', async ({ page }) => {
|
test('allow marking as inactive', async ({ page }) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -45,15 +45,20 @@ test('allow exempting from stale', async ({ page }) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('stale list considers periodicity', async ({ page }) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('page title has contact primary name', async ({ page }) => {
|
||||||
|
await expect(page.title()).toContain("Test Testerson");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
test('bullet points in free text display well', async ({ page }) => {
|
test('bullet points in free text display well', async ({ page }) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
twst('page title has contact primary name', async ({ page }) => {
|
|
||||||
await expect(page.title()).toContain("Test Testerson");
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
home: contact list scrolls in screen, not off screen
|
home: contact list scrolls in screen, not off screen
|
||||||
home: clicking off contact list closes it
|
home: clicking off contact list closes it
|
||||||
home: contact list is sorted ignoring case
|
home: contact list is sorted ignoring case
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use chrono::Local;
|
use jiff::{Timestamp, Unit, Zoned, civil, tz::TimeZone};
|
||||||
use sqlx::sqlite::SqliteRow;
|
use sqlx::sqlite::SqliteRow;
|
||||||
use sqlx::{FromRow, Row};
|
use sqlx::{FromRow, Row};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
@ -29,25 +29,31 @@ impl Display for Birthday {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Birthday {
|
impl Birthday {
|
||||||
pub fn next_occurrence(&self) -> Option<chrono::NaiveDate> {
|
pub fn next_occurrence(&self) -> Option<civil::Date> {
|
||||||
match &self {
|
match &self {
|
||||||
Birthday::Text(_) => None,
|
Birthday::Text(_) => None,
|
||||||
Birthday::Date(date) => Some(date.next_month_day_occurrence()?),
|
Birthday::Date(date) => date.next_month_day_occurrence(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn until_next(&self) -> Option<chrono::TimeDelta> {
|
pub fn until_next(&self) -> Option<jiff::Span> {
|
||||||
self.next_occurrence()
|
self.next_occurrence()
|
||||||
.map(|when| when.signed_duration_since(Local::now().date_naive()))
|
.map(|when| when.since(Zoned::now().date()).ok())?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// None if this is a text birthday or doesn't have a year
|
/// None if this is a text birthday or doesn't have a year
|
||||||
pub fn age(&self) -> Option<u32> {
|
pub fn age(&self) -> Option<i32> {
|
||||||
match &self {
|
match &self {
|
||||||
Birthday::Text(_) => None,
|
Birthday::Text(_) => None,
|
||||||
Birthday::Date(date) => date
|
Birthday::Date(date) => {
|
||||||
.to_date_naive()
|
let now = Timestamp::now().to_zoned(TimeZone::UTC);
|
||||||
.map(|birthdate| Local::now().date_naive().years_since(birthdate))?,
|
date.to_civil_date().map(|birthdate| {
|
||||||
|
now.since(&birthdate.to_zoned(TimeZone::UTC).unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.total((Unit::Year, &now))
|
||||||
|
.unwrap() as i32
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use chrono::{DateTime, NaiveDate, Utc};
|
use jiff::{Timestamp, civil};
|
||||||
use sqlx::sqlite::SqlitePool;
|
use sqlx::sqlite::SqlitePool;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ struct RawContact {
|
||||||
pub struct Contact {
|
pub struct Contact {
|
||||||
pub id: DbId,
|
pub id: DbId,
|
||||||
pub birthday: Option<Birthday>,
|
pub birthday: Option<Birthday>,
|
||||||
pub manually_freshened_at: Option<DateTime<Utc>>,
|
pub manually_freshened_at: Option<Timestamp>,
|
||||||
pub lives_with: String,
|
pub lives_with: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,8 +31,7 @@ impl Into<Contact> for RawContact {
|
||||||
.and_then(|s| Birthday::from_str(s.as_ref()).ok()),
|
.and_then(|s| Birthday::from_str(s.as_ref()).ok()),
|
||||||
manually_freshened_at: self
|
manually_freshened_at: self
|
||||||
.manually_freshened_at
|
.manually_freshened_at
|
||||||
.and_then(|str| DateTime::parse_from_str(str.as_ref(), "%+").ok())
|
.and_then(|str| str.parse::<Timestamp>().ok()),
|
||||||
.map(|d| d.to_utc()),
|
|
||||||
lives_with: self.lives_with,
|
lives_with: self.lives_with,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +49,7 @@ struct RawHydratedContact {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct HydratedContact {
|
pub struct HydratedContact {
|
||||||
pub contact: Contact,
|
pub contact: Contact,
|
||||||
pub last_mention_date: Option<NaiveDate>,
|
pub last_mention_date: Option<civil::Date>,
|
||||||
pub names: Vec<String>,
|
pub names: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +70,7 @@ impl Into<HydratedContact> for RawHydratedContact {
|
||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
last_mention_date: self
|
last_mention_date: self
|
||||||
.last_mention_date
|
.last_mention_date
|
||||||
.and_then(|str| NaiveDate::from_str(str.as_ref()).ok()),
|
.and_then(|str| str.parse::<civil::Date>().ok()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use chrono::NaiveDate;
|
use jiff::civil::Date;
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::sqlite::{SqlitePool, SqliteRow};
|
use sqlx::sqlite::{SqlitePool, SqliteRow};
|
||||||
|
|
@ -12,7 +12,7 @@ use crate::switchboard::{MentionHost, MentionHostType};
|
||||||
pub struct JournalEntry {
|
pub struct JournalEntry {
|
||||||
pub id: DbId,
|
pub id: DbId,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
pub date: NaiveDate,
|
pub date: Date,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Into<MentionHost<'a>> for &'a JournalEntry {
|
impl<'a> Into<MentionHost<'a>> for &'a JournalEntry {
|
||||||
|
|
@ -69,7 +69,7 @@ impl FromRow<'_, SqliteRow> for JournalEntry {
|
||||||
let id: DbId = row.try_get("id")?;
|
let id: DbId = row.try_get("id")?;
|
||||||
let value: String = row.try_get("value")?;
|
let value: String = row.try_get("value")?;
|
||||||
let date_str: &str = row.try_get("date")?;
|
let date_str: &str = row.try_get("date")?;
|
||||||
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").unwrap();
|
let date: Date = date_str.parse().unwrap();
|
||||||
Ok(Self { id, value, date })
|
Ok(Self { id, value, date })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use chrono::{Datelike, Local, NaiveDate};
|
use jiff::{Timestamp, civil::Date, tz::TimeZone};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use sqlx::{Database, Decode, Encode, Sqlite, encode::IsNull};
|
use sqlx::{Database, Decode, Encode, Sqlite, encode::IsNull};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
@ -6,38 +6,39 @@ use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct YearOptionalDate {
|
pub struct YearOptionalDate {
|
||||||
pub year: Option<i32>,
|
pub year: Option<i16>,
|
||||||
pub month: u32,
|
pub month: i8,
|
||||||
pub day: u32,
|
pub day: i8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl YearOptionalDate {
|
impl YearOptionalDate {
|
||||||
pub fn prev_month_day_occurrence(&self) -> Option<NaiveDate> {
|
pub fn prev_month_day_occurrence(&self) -> Option<Date> {
|
||||||
let now = Local::now();
|
let now = Timestamp::now().to_zoned(TimeZone::UTC);
|
||||||
let year = now.year();
|
let year = now.year();
|
||||||
let mut date = NaiveDate::from_ymd_opt(year, self.month, self.day);
|
Date::new(year, self.month, self.day).ok().and_then(|date| {
|
||||||
if let Some(real_date) = date {
|
if date >= now.date() {
|
||||||
if real_date >= now.date_naive() {
|
Date::new(year - 1, self.month, self.day).ok()
|
||||||
date = NaiveDate::from_ymd_opt(year - 1, self.month, self.day);
|
} else {
|
||||||
|
Some(date)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
date
|
|
||||||
}
|
|
||||||
pub fn next_month_day_occurrence(&self) -> Option<NaiveDate> {
|
|
||||||
let now = Local::now();
|
|
||||||
let year = now.year();
|
|
||||||
let mut date = NaiveDate::from_ymd_opt(year, self.month, self.day);
|
|
||||||
if let Some(real_date) = date {
|
|
||||||
if real_date < now.date_naive() {
|
|
||||||
date = NaiveDate::from_ymd_opt(year + 1, self.month, self.day);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
date
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_date_naive(&self) -> Option<NaiveDate> {
|
pub fn next_month_day_occurrence(&self) -> Option<Date> {
|
||||||
|
let now = Timestamp::now().to_zoned(TimeZone::UTC);
|
||||||
|
let year = now.year();
|
||||||
|
Date::new(year, self.month, self.day).ok().and_then(|date| {
|
||||||
|
if date < now.date() {
|
||||||
|
Date::new(year + 1, self.month, self.day).ok()
|
||||||
|
} else {
|
||||||
|
Some(date)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_civil_date(&self) -> Option<Date> {
|
||||||
if let Some(year) = self.year {
|
if let Some(year) = self.year {
|
||||||
NaiveDate::from_ymd_opt(year, self.month, self.day)
|
Date::new(year, self.month, self.day).ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -68,12 +69,12 @@ impl FromStr for YearOptionalDate {
|
||||||
let date_re = Regex::new(r"^([0-9]{4}|--)([0-9]{2})([0-9]{2})$").unwrap();
|
let date_re = Regex::new(r"^([0-9]{4}|--)([0-9]{2})([0-9]{2})$").unwrap();
|
||||||
if let Some(caps) = date_re.captures(str) {
|
if let Some(caps) = date_re.captures(str) {
|
||||||
let year_str = &caps[1];
|
let year_str = &caps[1];
|
||||||
let month = u32::from_str(&caps[2]).unwrap();
|
let month = i8::from_str(&caps[2]).unwrap();
|
||||||
let day = u32::from_str(&caps[3]).unwrap();
|
let day = i8::from_str(&caps[3]).unwrap();
|
||||||
let year = if year_str == "--" {
|
let year = if year_str == "--" {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(i32::from_str(year_str).unwrap())
|
Some(i16::from_str(year_str).unwrap())
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(Self { year, month, day });
|
return Ok(Self { year, month, day });
|
||||||
|
|
|
||||||
|
|
@ -109,14 +109,6 @@ impl Switchboard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(self: &mut Self, text: &String) {
|
|
||||||
self.trie.remove(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_mentionable(self: &mut Self, text: String, uri: String) {
|
|
||||||
self.trie.insert(text, uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_mentions<'a>(&self, host: impl Into<MentionHost<'a>>) -> HashSet<Mention> {
|
pub fn extract_mentions<'a>(&self, host: impl Into<MentionHost<'a>>) -> HashSet<Mention> {
|
||||||
let host: MentionHost = host.into();
|
let host: MentionHost = host.into();
|
||||||
let name_re = Regex::new(r"\[\[(.+?)\]\]").unwrap();
|
let name_re = Regex::new(r"\[\[(.+?)\]\]").unwrap();
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,19 @@ use axum::{
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use cache_bust::asset;
|
use cache_bust::asset;
|
||||||
use chrono::DateTime;
|
use jiff::{Timestamp, Unit, tz::TimeZone};
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use slug::slugify;
|
use slug::slugify;
|
||||||
use sqlx::{QueryBuilder, Sqlite};
|
use sqlx::QueryBuilder;
|
||||||
|
|
||||||
use super::Layout;
|
use super::Layout;
|
||||||
use super::home::journal_section;
|
use super::home::journal_section;
|
||||||
use crate::db::DbId;
|
use crate::db::DbId;
|
||||||
use crate::models::user::AuthSession;
|
use crate::models::user::AuthSession;
|
||||||
use crate::models::{HydratedContact, JournalEntry};
|
use crate::models::{HydratedContact, JournalEntry};
|
||||||
use crate::switchboard::{MentionHost, MentionHostType, insert_mentions, Switchboard};
|
use crate::switchboard::{MentionHost, MentionHostType, Switchboard, insert_mentions};
|
||||||
use crate::{AppError, AppState};
|
use crate::{AppError, AppState};
|
||||||
|
|
||||||
pub mod fields;
|
pub mod fields;
|
||||||
|
|
@ -40,22 +40,22 @@ pub fn router() -> Router<AppState> {
|
||||||
.route("/contact/{contact_id}/edit", get(self::get::contact_edit))
|
.route("/contact/{contact_id}/edit", get(self::get::contact_edit))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn human_delta(delta: &chrono::TimeDelta) -> String {
|
fn human_delta(span: &jiff::Span) -> String {
|
||||||
if delta.num_days() == 0 {
|
let todate = Timestamp::now().to_zoned(TimeZone::UTC).date();
|
||||||
return "today".to_string();
|
let span = span
|
||||||
}
|
.round(
|
||||||
|
jiff::SpanRound::new()
|
||||||
|
.largest(Unit::Year)
|
||||||
|
.smallest(Unit::Day)
|
||||||
|
.relative(todate),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut result = "in ".to_string();
|
if span.is_zero() {
|
||||||
let mut rem = delta.clone();
|
"today".to_string()
|
||||||
if rem.num_days().abs() >= 7 {
|
} else {
|
||||||
let weeks = rem.num_days() / 7;
|
format!("in {:#}", span)
|
||||||
rem -= chrono::TimeDelta::days(weeks * 7);
|
|
||||||
result.push_str(&format!("{}w ", weeks));
|
|
||||||
}
|
}
|
||||||
if rem.num_days().abs() > 0 {
|
|
||||||
result.push_str(&format!("{}d ", rem.num_days()));
|
|
||||||
}
|
|
||||||
result.trim().to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod get {
|
mod get {
|
||||||
|
|
@ -88,7 +88,9 @@ mod get {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let freshened = std::cmp::max(
|
let freshened = std::cmp::max(
|
||||||
contact.manually_freshened_at.map(|when| when.date_naive()),
|
contact
|
||||||
|
.manually_freshened_at
|
||||||
|
.map(|when| when.to_zoned(jiff::tz::TimeZone::UTC).date()),
|
||||||
entries.get(0).map(|entry| entry.date),
|
entries.get(0).map(|entry| entry.date),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -213,7 +215,7 @@ mod get {
|
||||||
let mfresh_str = contact
|
let mfresh_str = contact
|
||||||
.manually_freshened_at
|
.manually_freshened_at
|
||||||
.clone()
|
.clone()
|
||||||
.map_or("".to_string(), |m| m.to_rfc3339());
|
.map_or("".to_string(), |m| m.to_string());
|
||||||
|
|
||||||
let text_body: String =
|
let text_body: String =
|
||||||
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
||||||
|
|
@ -351,14 +353,15 @@ mod put {
|
||||||
Some(payload.birthday)
|
Some(payload.birthday)
|
||||||
};
|
};
|
||||||
|
|
||||||
let manually_freshened_at = if payload.manually_freshened_at.is_empty() {
|
let manually_freshened_at: Option<String> = if payload.manually_freshened_at.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(
|
Some(
|
||||||
DateTime::parse_from_str(&payload.manually_freshened_at, "%+")
|
payload
|
||||||
|
.manually_freshened_at
|
||||||
|
.parse::<Timestamp>()
|
||||||
.map_err(|_| anyhow::Error::msg("Could not parse freshened-at string"))?
|
.map_err(|_| anyhow::Error::msg("Could not parse freshened-at string"))?
|
||||||
.to_utc()
|
.to_string(),
|
||||||
.to_rfc3339(),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -386,9 +389,6 @@ mod put {
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if old_contact.text_body != text_body {
|
|
||||||
}
|
|
||||||
|
|
||||||
// these blocks are not in functions because payload gets progressively
|
// these blocks are not in functions because payload gets progressively
|
||||||
// partially moved as we handle each field and i don't want to deal with it
|
// partially moved as we handle each field and i don't want to deal with it
|
||||||
|
|
||||||
|
|
@ -488,22 +488,18 @@ mod put {
|
||||||
let old_names: Vec<String> = old_names.into_iter().map(|(s,)| s).collect();
|
let old_names: Vec<String> = old_names.into_iter().map(|(s,)| s).collect();
|
||||||
|
|
||||||
if old_names != new_names {
|
if old_names != new_names {
|
||||||
sqlx::query!(
|
sqlx::query!("delete from names where contact_id = $1", contact_id)
|
||||||
"delete from names where contact_id = $1",
|
|
||||||
contact_id
|
|
||||||
)
|
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !new_names.is_empty() {
|
if !new_names.is_empty() {
|
||||||
QueryBuilder::new(
|
QueryBuilder::new("insert into names (contact_id, sort, name) ")
|
||||||
"insert into names (contact_id, sort, name) "
|
.push_values(new_names.iter().enumerate(), |mut b, (sort, name)| {
|
||||||
).push_values(new_names.iter().enumerate(), |mut b, (sort, name)| {
|
b.push_bind(contact_id)
|
||||||
b
|
|
||||||
.push_bind(contact_id)
|
|
||||||
.push_bind(DbId::try_from(sort).unwrap())
|
.push_bind(DbId::try_from(sort).unwrap())
|
||||||
.push_bind(name);
|
.push_bind(name);
|
||||||
}).build()
|
})
|
||||||
|
.build()
|
||||||
.persistent(false)
|
.persistent(false)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -524,10 +520,7 @@ mod put {
|
||||||
let old_groups: Vec<String> = old_groups.into_iter().map(|(s,)| s).collect();
|
let old_groups: Vec<String> = old_groups.into_iter().map(|(s,)| s).collect();
|
||||||
|
|
||||||
if new_groups != old_groups {
|
if new_groups != old_groups {
|
||||||
sqlx::query!(
|
sqlx::query!("delete from groups where contact_id = $1", contact_id)
|
||||||
"delete from groups where contact_id = $1",
|
|
||||||
contact_id
|
|
||||||
)
|
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -566,7 +559,6 @@ mod put {
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if regen_text_body {
|
if regen_text_body {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"delete from mentions where entity_id = $1 and entity_type = $2",
|
"delete from mentions where entity_id = $1 and entity_type = $2",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use cache_bust::asset;
|
use cache_bust::asset;
|
||||||
use chrono::{Local, NaiveDate, TimeDelta};
|
use jiff::{Timestamp, Unit, Zoned, civil, tz::TimeZone};
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
use sqlx::sqlite::SqlitePool;
|
use sqlx::sqlite::SqlitePool;
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ use crate::{AppError, AppState};
|
||||||
struct ContactFreshness {
|
struct ContactFreshness {
|
||||||
contact_id: DbId,
|
contact_id: DbId,
|
||||||
display: String,
|
display: String,
|
||||||
fresh_date: NaiveDate,
|
fresh_date: civil::Date,
|
||||||
fresh_str: String,
|
fresh_str: String,
|
||||||
elapsed_str: String,
|
elapsed_str: String,
|
||||||
}
|
}
|
||||||
|
|
@ -46,8 +46,8 @@ fn freshness_section(freshens: &Vec<ContactFreshness>) -> Result<Markup, AppErro
|
||||||
struct KnownBirthdayContact {
|
struct KnownBirthdayContact {
|
||||||
contact_id: i64,
|
contact_id: i64,
|
||||||
display: String,
|
display: String,
|
||||||
prev_birthday: NaiveDate,
|
prev_birthday: civil::Date,
|
||||||
next_birthday: NaiveDate,
|
next_birthday: civil::Date,
|
||||||
}
|
}
|
||||||
fn birthdays_section(
|
fn birthdays_section(
|
||||||
prev_birthdays: &Vec<KnownBirthdayContact>,
|
prev_birthdays: &Vec<KnownBirthdayContact>,
|
||||||
|
|
@ -64,7 +64,7 @@ fn birthdays_section(
|
||||||
(contact.display)
|
(contact.display)
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
(contact.next_birthday.format("%m-%d"))
|
(contact.next_birthday.strftime("%m-%d"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +75,7 @@ fn birthdays_section(
|
||||||
(contact.display)
|
(contact.display)
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
(contact.prev_birthday.format("%m-%d"))
|
(contact.prev_birthday.strftime("%m-%d"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +103,7 @@ pub async fn journal_section(
|
||||||
added to the top of the list regardless of date; refresh the page to re-sort."
|
added to the top of the list regardless of date; refresh the page to re-sort."
|
||||||
}
|
}
|
||||||
form hx-post="/journal_entry" hx-target="next .entries" hx-target-error="#journal-error" hx-swap="afterbegin" hx-on::after-request="if(event.detail.successful) this.reset()" {
|
form hx-post="/journal_entry" hx-target="next .entries" hx-target-error="#journal-error" hx-swap="afterbegin" hx-on::after-request="if(event.detail.successful) this.reset()" {
|
||||||
input name="date" placeholder=(Local::now().date_naive().to_string());
|
input name="date" placeholder=(Zoned::now().date().to_string());
|
||||||
textarea name="value" placeholder="New entry..." autofocus {}
|
textarea name="value" placeholder="New entry..." autofocus {}
|
||||||
input type="submit" value="Add Entry";
|
input type="submit" value="Add Entry";
|
||||||
}
|
}
|
||||||
|
|
@ -135,11 +135,11 @@ pub mod get {
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|contact| {
|
.map(|contact| {
|
||||||
let zero = NaiveDate::from_epoch_days(0).unwrap();
|
let zero = jiff::civil::Date::ZERO;
|
||||||
let fresh_date = std::cmp::max(
|
let fresh_date = std::cmp::max(
|
||||||
contact
|
contact
|
||||||
.manually_freshened_at
|
.manually_freshened_at
|
||||||
.map(|x| x.date_naive())
|
.map(|ts| ts.to_zoned(TimeZone::UTC).date())
|
||||||
.unwrap_or(zero),
|
.unwrap_or(zero),
|
||||||
contact.last_mention_date.unwrap_or(zero),
|
contact.last_mention_date.unwrap_or(zero),
|
||||||
);
|
);
|
||||||
|
|
@ -152,30 +152,23 @@ pub mod get {
|
||||||
elapsed_str: "".to_string(),
|
elapsed_str: "".to_string(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut duration = Local::now().date_naive().signed_duration_since(fresh_date);
|
let utc = TimeZone::UTC;
|
||||||
let mut elapsed: Vec<String> = Vec::new();
|
let todate = Timestamp::now().to_zoned(utc.clone()).date();
|
||||||
let y = duration.num_weeks() / 52;
|
let duration = todate
|
||||||
let count = |n: i64, noun: &str| {
|
.since(&fresh_date.to_zoned(utc).unwrap())
|
||||||
format!("{} {}{}", n, noun, if n > 1 { "s" } else { "" })
|
.unwrap()
|
||||||
};
|
.round(
|
||||||
if y > 0 {
|
jiff::SpanRound::new()
|
||||||
elapsed.push(count(y, "year"));
|
.largest(Unit::Year)
|
||||||
duration -= TimeDelta::weeks(y * 52);
|
.smallest(Unit::Day)
|
||||||
}
|
.relative(todate),
|
||||||
let w = duration.num_weeks();
|
)
|
||||||
if w > 0 {
|
.unwrap();
|
||||||
elapsed.push(count(w, "week"));
|
|
||||||
duration -= TimeDelta::weeks(w);
|
|
||||||
}
|
|
||||||
let d = duration.num_days();
|
|
||||||
if d > 0 {
|
|
||||||
elapsed.push(count(d, "day"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let elapsed_str = if elapsed.is_empty() {
|
let elapsed_str = if duration.is_zero() {
|
||||||
"today".to_string()
|
"today".to_string()
|
||||||
} else {
|
} else {
|
||||||
elapsed.join(", ")
|
format!("{:#}", duration)
|
||||||
};
|
};
|
||||||
|
|
||||||
ContactFreshness {
|
ContactFreshness {
|
||||||
|
|
@ -197,8 +190,8 @@ pub mod get {
|
||||||
Some(KnownBirthdayContact {
|
Some(KnownBirthdayContact {
|
||||||
contact_id: contact.id,
|
contact_id: contact.id,
|
||||||
display: contact.display_name(),
|
display: contact.display_name(),
|
||||||
prev_birthday: date.prev_month_day_occurrence().unwrap(),
|
prev_birthday: date.prev_month_day_occurrence()?,
|
||||||
next_birthday: date.next_month_day_occurrence().unwrap(),
|
next_birthday: date.next_month_day_occurrence()?,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,9 @@ mod get {
|
||||||
for contact in &contacts {
|
for contact in &contacts {
|
||||||
if let Some(Birthday::Date(yo_date)) = &contact.birthday {
|
if let Some(Birthday::Date(yo_date)) = &contact.birthday {
|
||||||
if let Some(date) = NaiveDate::from_ymd_opt(
|
if let Some(date) = NaiveDate::from_ymd_opt(
|
||||||
yo_date.year.unwrap_or(1900),
|
yo_date.year.unwrap_or(1900).into(),
|
||||||
yo_date.month,
|
yo_date.month.try_into().unwrap(),
|
||||||
yo_date.day,
|
yo_date.day.try_into().unwrap(),
|
||||||
) {
|
) {
|
||||||
calendar.push(
|
calendar.push(
|
||||||
Event::new()
|
Event::new()
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ use axum::{
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{delete, patch, post},
|
routing::{delete, patch, post},
|
||||||
};
|
};
|
||||||
use chrono::{Datelike, Local, NaiveDate};
|
use jiff::{Zoned, civil::Date};
|
||||||
use maud::Markup;
|
use maud::Markup;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::models::JournalEntry;
|
use crate::models::JournalEntry;
|
||||||
use crate::models::user::AuthSession;
|
use crate::models::user::AuthSession;
|
||||||
use crate::switchboard::{MentionHost, MentionHostType, insert_mentions};
|
use crate::switchboard::{MentionHostType, insert_mentions};
|
||||||
use crate::{AppError, AppState, DbId};
|
use crate::{AppError, AppState, DbId};
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
|
|
@ -39,10 +39,10 @@ mod post {
|
||||||
let pool = &state.db(&user).pool;
|
let pool = &state.db(&user).pool;
|
||||||
let sw_lock = state.switchboard(&user);
|
let sw_lock = state.switchboard(&user);
|
||||||
|
|
||||||
let now = Local::now().date_naive();
|
let now = Zoned::now();
|
||||||
|
|
||||||
let date = if payload.date.is_empty() {
|
let date = if payload.date.is_empty() {
|
||||||
now
|
now.date()
|
||||||
} else {
|
} else {
|
||||||
let date_re =
|
let date_re =
|
||||||
Regex::new(r"^(?:(?<year>[0-9]{4})-)?(?:(?<month>[0-9]{2})-)?(?<day>[0-9]{2})$")
|
Regex::new(r"^(?:(?<year>[0-9]{4})-)?(?:(?<month>[0-9]{2})-)?(?<day>[0-9]{2})$")
|
||||||
|
|
@ -54,17 +54,16 @@ mod post {
|
||||||
// unwrapping these parses is safe since it's matching [0-9]{2,4}
|
// unwrapping these parses is safe since it's matching [0-9]{2,4}
|
||||||
let year = caps
|
let year = caps
|
||||||
.name("year")
|
.name("year")
|
||||||
.map(|m| m.as_str().parse::<i32>().unwrap())
|
.map(|m| m.as_str().parse::<i16>().unwrap())
|
||||||
.unwrap_or(now.year());
|
.unwrap_or(now.year());
|
||||||
let month = caps
|
let month = caps
|
||||||
.name("month")
|
.name("month")
|
||||||
.map(|m| m.as_str().parse::<u32>().unwrap())
|
.map(|m| m.as_str().parse::<i8>().unwrap())
|
||||||
.unwrap_or(now.month());
|
.unwrap_or(now.month());
|
||||||
let day = caps.name("day").unwrap().as_str().parse::<u32>().unwrap();
|
let day = caps.name("day").unwrap().as_str().parse::<i8>().unwrap();
|
||||||
|
|
||||||
NaiveDate::from_ymd_opt(year, month, day).ok_or(anyhow::Error::msg(
|
Date::new(year, month, day)
|
||||||
"invalid date: failed NaiveDate construction",
|
.map_err(|_| anyhow::Error::msg("invalid date: failed NaiveDate construction"))?
|
||||||
))?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// not a macro query, we want to use JournalEntry's custom FromRow
|
// not a macro query, we want to use JournalEntry's custom FromRow
|
||||||
|
|
@ -131,7 +130,6 @@ mod patch {
|
||||||
insert_mentions(&mentions, pool).await?;
|
insert_mentions(&mentions, pool).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(new_entry.to_html(pool).await?)
|
Ok(new_entry.to_html(pool).await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue