2025-11-27 13:45:21 -06:00
|
|
|
|
use axum::{
|
|
|
|
|
|
Router,
|
|
|
|
|
|
extract::{State, path::Path},
|
|
|
|
|
|
http::HeaderMap,
|
|
|
|
|
|
response::IntoResponse,
|
|
|
|
|
|
routing::{delete, get, post, put},
|
|
|
|
|
|
};
|
|
|
|
|
|
use axum_extra::extract::Form;
|
2025-12-01 15:23:56 -06:00
|
|
|
|
use cache_bust::asset;
|
2025-11-27 13:45:21 -06:00
|
|
|
|
use chrono::DateTime;
|
|
|
|
|
|
use maud::{Markup, PreEscaped, html};
|
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
use sqlx::{QueryBuilder, Sqlite};
|
|
|
|
|
|
|
|
|
|
|
|
use super::Layout;
|
|
|
|
|
|
use super::home::journal_section;
|
|
|
|
|
|
use crate::db::DbId;
|
|
|
|
|
|
use crate::models::user::AuthSession;
|
|
|
|
|
|
use crate::models::{HydratedContact, JournalEntry};
|
|
|
|
|
|
use crate::{AppError, AppState};
|
|
|
|
|
|
|
2025-11-28 17:05:06 -06:00
|
|
|
|
#[derive(serde::Serialize, Debug)]
|
2025-11-27 13:45:21 -06:00
|
|
|
|
pub struct Address {
|
|
|
|
|
|
pub id: DbId,
|
|
|
|
|
|
pub contact_id: DbId,
|
|
|
|
|
|
pub label: Option<String>,
|
|
|
|
|
|
pub value: String,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn router() -> Router<AppState> {
|
|
|
|
|
|
Router::new()
|
|
|
|
|
|
.route("/contact/new", post(self::post::contact))
|
|
|
|
|
|
.route("/contact/{contact_id}", get(self::get::contact))
|
|
|
|
|
|
.route("/contact/{contact_id}", put(self::put::contact))
|
|
|
|
|
|
.route("/contact/{contact_id}", delete(self::delete::contact))
|
|
|
|
|
|
.route("/contact/{contact_id}/edit", get(self::get::contact_edit))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn human_delta(delta: &chrono::TimeDelta) -> String {
|
|
|
|
|
|
if delta.num_days() == 0 {
|
|
|
|
|
|
return "today".to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut result = "in ".to_string();
|
|
|
|
|
|
let mut rem = delta.clone();
|
|
|
|
|
|
if rem.num_days().abs() >= 7 {
|
|
|
|
|
|
let weeks = rem.num_days() / 7;
|
|
|
|
|
|
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 {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
pub async fn contact(
|
|
|
|
|
|
auth_session: AuthSession,
|
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
|
Path(contact_id): Path<u32>,
|
|
|
|
|
|
layout: Layout,
|
|
|
|
|
|
) -> Result<Markup, AppError> {
|
|
|
|
|
|
let pool = &state.db(&auth_session.user.unwrap()).pool;
|
|
|
|
|
|
let contact: HydratedContact = sqlx::query_as(
|
|
|
|
|
|
"select id, birthday, manually_freshened_at, (
|
|
|
|
|
|
select string_agg(name,'\x1c' order by sort)
|
|
|
|
|
|
from names where contact_id = c.id
|
|
|
|
|
|
) as names
|
|
|
|
|
|
from contacts c
|
|
|
|
|
|
where c.id = $1",
|
|
|
|
|
|
)
|
|
|
|
|
|
.bind(contact_id)
|
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
let entries: Vec<JournalEntry> = sqlx::query_as(
|
|
|
|
|
|
"select j.id, j.value, j.date from journal_entries j
|
|
|
|
|
|
join contact_mentions cm on j.id = cm.entry_id
|
|
|
|
|
|
where cm.contact_id = $1",
|
|
|
|
|
|
)
|
|
|
|
|
|
.bind(contact_id)
|
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
let addresses: Vec<Address> = sqlx::query_as!(
|
|
|
|
|
|
Address,
|
|
|
|
|
|
"select * from addresses where contact_id = $1",
|
|
|
|
|
|
contact_id
|
|
|
|
|
|
)
|
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
2025-11-27 15:03:22 -06:00
|
|
|
|
let text_body: Option<String> =
|
|
|
|
|
|
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
|
.await?
|
|
|
|
|
|
.text_body;
|
|
|
|
|
|
|
2025-11-27 13:45:21 -06:00
|
|
|
|
Ok(layout.render(
|
2025-12-01 15:23:56 -06:00
|
|
|
|
Some(vec![asset!("contact.css"), asset!("journal.css")]),
|
2025-11-27 13:45:21 -06:00
|
|
|
|
html! {
|
|
|
|
|
|
a href=(format!("/contact/{}/edit", contact_id)) { "Edit" }
|
|
|
|
|
|
|
|
|
|
|
|
div id="fields" {
|
|
|
|
|
|
label { @if contact.names.len() > 1 { "names" } @else { "name" }}
|
|
|
|
|
|
div {
|
|
|
|
|
|
@for name in &contact.names {
|
|
|
|
|
|
div { (name) }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
@if let Some(bday) = &contact.birthday {
|
|
|
|
|
|
label { "birthday" }
|
|
|
|
|
|
div {
|
|
|
|
|
|
(bday.to_string())
|
|
|
|
|
|
@if let Some(delta) = &bday.until_next() {
|
|
|
|
|
|
" ("
|
|
|
|
|
|
(human_delta(delta))
|
|
|
|
|
|
@if let Some(age) = &bday.age() {
|
|
|
|
|
|
", turning " (age + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
")"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
label { "freshened" }
|
|
|
|
|
|
div {
|
|
|
|
|
|
@if let Some(when) = &contact.manually_freshened_at {
|
|
|
|
|
|
(when.date_naive().to_string())
|
|
|
|
|
|
} @else {
|
|
|
|
|
|
"(never)"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
@if addresses.len() == 1 {
|
|
|
|
|
|
label { "address" }
|
|
|
|
|
|
#addresses {
|
2025-11-28 08:40:43 -06:00
|
|
|
|
.label {}
|
2025-11-27 13:45:21 -06:00
|
|
|
|
.value { (addresses[0].value) }
|
|
|
|
|
|
}
|
|
|
|
|
|
} @else if addresses.len() > 0 {
|
|
|
|
|
|
label { "addresses" }
|
|
|
|
|
|
#addresses {
|
|
|
|
|
|
@for address in addresses {
|
2025-11-28 08:40:43 -06:00
|
|
|
|
@let lbl = address.label.unwrap_or(String::new());
|
|
|
|
|
|
.label data-is-empty=(lbl.len() == 0) {
|
|
|
|
|
|
(lbl)
|
2025-11-27 13:45:21 -06:00
|
|
|
|
}
|
|
|
|
|
|
.value { (address.value) }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 15:03:22 -06:00
|
|
|
|
|
|
|
|
|
|
@if let Some(text_body) = text_body {
|
|
|
|
|
|
@if text_body.len() > 0 {
|
|
|
|
|
|
#text_body { (PreEscaped(markdown::to_html(&text_body))) }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 13:45:21 -06:00
|
|
|
|
(journal_section(pool, &entries).await?)
|
|
|
|
|
|
},
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub async fn contact_edit(
|
|
|
|
|
|
auth_session: AuthSession,
|
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
|
Path(contact_id): Path<u32>,
|
|
|
|
|
|
layout: Layout,
|
|
|
|
|
|
) -> Result<Markup, AppError> {
|
|
|
|
|
|
let pool = &state.db(&auth_session.user.unwrap()).pool;
|
|
|
|
|
|
let contact: HydratedContact = sqlx::query_as(
|
|
|
|
|
|
"select id, birthday, manually_freshened_at, (
|
|
|
|
|
|
select string_agg(name,'\x1c' order by sort)
|
|
|
|
|
|
from names where contact_id = c.id
|
|
|
|
|
|
) as names, (
|
|
|
|
|
|
select jes.date from journal_entries jes
|
|
|
|
|
|
join contact_mentions cms on cms.entry_id = jes.id
|
|
|
|
|
|
where cms.contact_id = c.id
|
|
|
|
|
|
order by jes.date desc limit 1
|
|
|
|
|
|
) as last_mention_date from contacts c
|
|
|
|
|
|
where c.id = $1",
|
|
|
|
|
|
)
|
|
|
|
|
|
.bind(contact_id)
|
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
let addresses: Vec<Address> = sqlx::query_as!(
|
|
|
|
|
|
Address,
|
|
|
|
|
|
"select * from addresses where contact_id = $1",
|
|
|
|
|
|
contact_id
|
|
|
|
|
|
)
|
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
let cid_url = format!("/contact/{}", contact.id);
|
|
|
|
|
|
let mfresh_str = contact
|
|
|
|
|
|
.manually_freshened_at
|
|
|
|
|
|
.clone()
|
|
|
|
|
|
.map_or("".to_string(), |m| m.to_rfc3339());
|
2025-11-27 15:03:22 -06:00
|
|
|
|
|
|
|
|
|
|
let text_body: String =
|
|
|
|
|
|
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
|
.await?
|
|
|
|
|
|
.text_body
|
|
|
|
|
|
.unwrap_or(String::new());
|
|
|
|
|
|
|
2025-12-01 15:23:56 -06:00
|
|
|
|
Ok(layout.render(Some(vec![asset!("contact.css")]), html! {
|
2025-11-27 13:45:21 -06:00
|
|
|
|
form hx-ext="response-targets" {
|
|
|
|
|
|
div {
|
|
|
|
|
|
input type="button" value="Save" hx-put=(cid_url) hx-target-error="#error";
|
|
|
|
|
|
input type="button" value="Delete" hx-delete=(cid_url) hx-target-error="#error";
|
|
|
|
|
|
div #error;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
div #fields {
|
|
|
|
|
|
label { @if contact.names.len() > 1 { "names" } @else { "name" }}
|
|
|
|
|
|
div #names x-data=(format!("{{ names: {:?}, new_name: '' }}", &contact.names)) {
|
|
|
|
|
|
template x-for="(name, idx) in names" {
|
|
|
|
|
|
div {
|
|
|
|
|
|
input name="name" x-model="name";
|
|
|
|
|
|
input type="button" value="×" x-bind:disabled="idx == 0" x-on:click="names.splice(idx, 1)";
|
|
|
|
|
|
input type="button" value="↑" x-bind:disabled="idx == 0" x-on:click="[names[idx-1], names[idx]] = [names[idx], names[idx-1]]";
|
|
|
|
|
|
input type="button" value="↓" x-bind:disabled="idx == names.length - 1" x-on:click="[names[idx+1], names[idx]] = [names[idx], names[idx+1]]";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
div {
|
|
|
|
|
|
input name="name" x-model="new_name" placeholder="New name";
|
|
|
|
|
|
input type="button" value="Add" x-on:click="names.push(new_name); new_name = ''";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
label { "birthday" }
|
|
|
|
|
|
div {
|
|
|
|
|
|
input name="birthday" value=(contact.birthday.clone().map_or("".to_string(), |b| b.serialize()));
|
|
|
|
|
|
span .hint { code { "(yyyy|--)mmdd" } " or free text" }
|
|
|
|
|
|
}
|
|
|
|
|
|
label { "freshened" }
|
|
|
|
|
|
div x-data=(json!({ "date": mfresh_str })) {
|
|
|
|
|
|
input type="hidden" name="manually_freshened_at" x-model="date";
|
|
|
|
|
|
span x-text="date.length ? date.split('T')[0] : '(never)'" {}
|
|
|
|
|
|
input type="button" value="Mark fresh now" x-on:click="date = new Date().toISOString()";
|
|
|
|
|
|
}
|
|
|
|
|
|
label { "addresses" }
|
|
|
|
|
|
div x-data=(json!({ "addresses": addresses, "new_label": "", "new_address": "" })) {
|
|
|
|
|
|
template x-for="(address, index) in addresses" x-bind:key="index" {
|
2025-11-28 17:05:06 -06:00
|
|
|
|
.address-input {
|
|
|
|
|
|
input name="address_label" x-show="addresses.length" x-model="address.label" placeholder="label";
|
|
|
|
|
|
.grow-wrap x-bind:data-replicated-value="address.value" {
|
|
|
|
|
|
textarea name="address_value" x-model="address.value" placeholder="address" {}
|
|
|
|
|
|
}
|
2025-11-27 13:45:21 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-28 17:05:06 -06:00
|
|
|
|
.address-input {
|
|
|
|
|
|
input x-show="addresses.length" name="address_label" x-model="new_label" placeholder="label";
|
|
|
|
|
|
.grow-wrap x-bind:data-replicated-value="new_address" {
|
|
|
|
|
|
textarea name="address_value" x-model="new_address" placeholder="new address" {}
|
|
|
|
|
|
}
|
2025-11-27 13:45:21 -06:00
|
|
|
|
}
|
|
|
|
|
|
input type="button" value="Add" x-on:click="addresses.push({ label: new_label, value: new_address }); new_label = ''; new_address = ''";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-28 17:05:06 -06:00
|
|
|
|
div #text_body {
|
|
|
|
|
|
div { "Free text (supports markdown)" }
|
|
|
|
|
|
.grow-wrap data-replicated-value=(text_body) {
|
|
|
|
|
|
textarea name="text_body"
|
|
|
|
|
|
onInput="this.parentNode.dataset.replicatedValue = this.value"
|
|
|
|
|
|
{ (text_body) }
|
|
|
|
|
|
}
|
2025-11-27 17:39:59 -06:00
|
|
|
|
}
|
2025-11-27 13:45:21 -06:00
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mod post {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
pub async fn contact(
|
|
|
|
|
|
auth_session: AuthSession,
|
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
|
let user = auth_session.user.unwrap();
|
|
|
|
|
|
let pool = &state.db(&user).pool;
|
|
|
|
|
|
|
|
|
|
|
|
let contact_id: (u32,) =
|
|
|
|
|
|
sqlx::query_as("insert into contacts (birthday) values (null) returning id")
|
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
|
headers.insert(
|
|
|
|
|
|
"HX-Redirect",
|
|
|
|
|
|
format!("/contact/{}/edit", contact_id.0).parse()?,
|
|
|
|
|
|
);
|
|
|
|
|
|
Ok((headers, "ok"))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mod put {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
pub struct PutContact {
|
|
|
|
|
|
name: Option<Vec<String>>,
|
|
|
|
|
|
birthday: String,
|
|
|
|
|
|
manually_freshened_at: String,
|
|
|
|
|
|
address_label: Option<Vec<String>>,
|
|
|
|
|
|
address_value: Option<Vec<String>>,
|
2025-11-27 15:03:22 -06:00
|
|
|
|
text_body: String,
|
2025-11-27 13:45:21 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub async fn contact(
|
|
|
|
|
|
auth_session: AuthSession,
|
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
|
Path(contact_id): Path<u32>,
|
|
|
|
|
|
Form(payload): Form<PutContact>,
|
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
|
let user = auth_session.user.unwrap();
|
|
|
|
|
|
let pool = &state.db(&user).pool;
|
|
|
|
|
|
|
|
|
|
|
|
let birthday = if payload.birthday.is_empty() {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some(payload.birthday)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let manually_freshened_at = if payload.manually_freshened_at.is_empty() {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some(
|
|
|
|
|
|
DateTime::parse_from_str(&payload.manually_freshened_at, "%+")
|
|
|
|
|
|
.map_err(|_| anyhow::Error::msg("Could not parse freshened-at string"))?
|
|
|
|
|
|
.to_utc()
|
|
|
|
|
|
.to_rfc3339(),
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-27 15:03:22 -06:00
|
|
|
|
let text_body = if payload.text_body.is_empty() {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some(payload.text_body)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-27 13:45:21 -06:00
|
|
|
|
sqlx::query!(
|
2025-11-27 15:03:22 -06:00
|
|
|
|
"update contacts set (birthday, manually_freshened_at, text_body) = ($1, $2, $3) where id = $4",
|
2025-11-27 13:45:21 -06:00
|
|
|
|
birthday,
|
|
|
|
|
|
manually_freshened_at,
|
2025-11-27 15:03:22 -06:00
|
|
|
|
text_body,
|
2025-11-27 13:45:21 -06:00
|
|
|
|
contact_id
|
|
|
|
|
|
)
|
|
|
|
|
|
.execute(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
// update addresses
|
|
|
|
|
|
sqlx::query!("delete from addresses where contact_id = $1", contact_id)
|
|
|
|
|
|
.execute(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(values) = payload.address_value {
|
|
|
|
|
|
let labels = if values.len() == 1 {
|
|
|
|
|
|
Some(vec![String::new()])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
payload.address_label
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(labels) = labels {
|
|
|
|
|
|
let new_addresses = labels
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.zip(values)
|
|
|
|
|
|
.filter(|(_, val)| val.len() > 0);
|
|
|
|
|
|
for (label, value) in new_addresses {
|
|
|
|
|
|
sqlx::query!(
|
|
|
|
|
|
"insert into addresses (contact_id, label, value) values ($1, $2, $3)",
|
|
|
|
|
|
contact_id,
|
|
|
|
|
|
label,
|
|
|
|
|
|
value
|
|
|
|
|
|
)
|
|
|
|
|
|
.execute(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let old_names: Vec<(String,)> = sqlx::query_as(
|
|
|
|
|
|
"delete from contact_mentions;
|
|
|
|
|
|
delete from names where contact_id = $1 returning name;",
|
|
|
|
|
|
)
|
|
|
|
|
|
.bind(contact_id)
|
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
let mut recalc_counts: QueryBuilder<Sqlite> = QueryBuilder::new(
|
|
|
|
|
|
"select name, contact_id from (
|
|
|
|
|
|
select name, contact_id, count(name) as ct from names where name in (",
|
|
|
|
|
|
);
|
|
|
|
|
|
let mut name_list = recalc_counts.separated(", ");
|
|
|
|
|
|
for (name,) in &old_names {
|
|
|
|
|
|
name_list.push_bind(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(names) = payload.name {
|
|
|
|
|
|
let names: Vec<String> = names.into_iter().filter(|n| n.len() > 0).collect();
|
|
|
|
|
|
if !names.is_empty() {
|
|
|
|
|
|
for name in &names {
|
|
|
|
|
|
name_list.push_bind(name.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut name_insert: QueryBuilder<Sqlite> =
|
|
|
|
|
|
QueryBuilder::new("insert into names (contact_id, sort, name) ");
|
|
|
|
|
|
name_insert.push_values(names.iter().enumerate(), |mut builder, (sort, name)| {
|
|
|
|
|
|
builder
|
|
|
|
|
|
.push_bind(contact_id)
|
|
|
|
|
|
.push_bind(DbId::try_from(sort).unwrap())
|
|
|
|
|
|
.push_bind(name);
|
|
|
|
|
|
});
|
|
|
|
|
|
name_insert.build().persistent(false).execute(pool).await?;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
name_list.push_unseparated(") group by name) where ct = 1");
|
|
|
|
|
|
let recalc_names: Vec<(String, DbId)> = recalc_counts
|
|
|
|
|
|
.build_query_as()
|
|
|
|
|
|
.persistent(false)
|
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
let trie_mutex = state.contact_search(&user);
|
|
|
|
|
|
let mut trie = trie_mutex.write().unwrap();
|
|
|
|
|
|
for name in &old_names {
|
|
|
|
|
|
trie.remove(&name.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for name in recalc_names {
|
|
|
|
|
|
trie.insert(name.0, name.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let journal_entries: Vec<JournalEntry> = sqlx::query_as("select * from journal_entries")
|
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
for entry in journal_entries {
|
|
|
|
|
|
entry
|
|
|
|
|
|
.insert_mentions(state.contact_search(&user), pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
|
headers.insert("HX-Redirect", format!("/contact/{}", contact_id).parse()?);
|
|
|
|
|
|
Ok((headers, "ok"))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
mod delete {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
pub async fn contact(
|
|
|
|
|
|
auth_session: AuthSession,
|
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
|
Path(contact_id): Path<u32>,
|
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
|
let user = auth_session.user.unwrap();
|
|
|
|
|
|
let pool = &state.db(&user).pool;
|
|
|
|
|
|
|
|
|
|
|
|
sqlx::query(
|
|
|
|
|
|
"delete from contact_mentions where contact_id = $1;
|
|
|
|
|
|
delete from names where contact_id = $1;
|
|
|
|
|
|
delete from contacts where id = $1;",
|
|
|
|
|
|
)
|
|
|
|
|
|
.bind(contact_id)
|
|
|
|
|
|
.execute(pool)
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
|
headers.insert("HX-Redirect", "/".parse()?);
|
|
|
|
|
|
Ok((headers, "ok"))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|