fix,feat: mention behavior and page titles

This commit is contained in:
Robert Perce 2026-02-14 13:35:59 -06:00
parent 7e2f5d0a18
commit 79a054ab40
22 changed files with 314 additions and 140 deletions

View file

@ -19,7 +19,7 @@ use super::home::journal_section;
use crate::db::DbId;
use crate::models::user::AuthSession;
use crate::models::{HydratedContact, JournalEntry};
use crate::switchboard::{MentionHost, MentionHostType, insert_mentions};
use crate::switchboard::{MentionHost, MentionHostType, insert_mentions, Switchboard};
use crate::{AppError, AppState};
pub mod fields;
@ -87,6 +87,11 @@ mod get {
.fetch_all(pool)
.await?;
let freshened = std::cmp::max(
contact.manually_freshened_at.map(|when| when.date_naive()),
entries.get(0).map(|entry| entry.date),
);
let phone_numbers: Vec<PhoneNumber> = sqlx::query_as!(
PhoneNumber,
"select * from phone_numbers where contact_id = $1",
@ -113,6 +118,7 @@ mod get {
.text_body;
Ok(layout.render(
contact.names.get(0).unwrap_or(&String::from("(unknown)")),
Some(vec![asset!("contact.css"), asset!("journal.css")]),
html! {
a href=(format!("/contact/{}/edit", contact_id)) { "Edit" }
@ -141,8 +147,8 @@ mod get {
}
label { "freshened" }
div {
@if let Some(when) = &contact.manually_freshened_at {
(when.date_naive().to_string())
@if let Some(freshened) = freshened {
(freshened.to_string())
} @else {
"(never)"
}
@ -216,7 +222,10 @@ mod get {
.text_body
.unwrap_or(String::new());
Ok(layout.render(Some(vec![asset!("contact.css")]), html! {
Ok(layout.render(
format!("Edit: {}", contact.names.get(0).unwrap_or(&String::from("(unknown)"))),
Some(vec![asset!("contact.css")]),
html! {
form hx-ext="response-targets" {
div {
input type="button" value="Save" hx-put=(cid_url) hx-target-error="#error";
@ -377,50 +386,7 @@ mod put {
.execute(pool)
.await?;
if old_contact.lives_with != payload.lives_with {
sqlx::query!(
"delete from mentions where entity_id = $1 and entity_type = $2",
contact_id,
MentionHostType::ContactLivesWith as DbId
)
.execute(pool)
.await?;
let mention_host = MentionHost {
entity_id: contact_id,
entity_type: MentionHostType::ContactLivesWith as DbId,
input: &payload.lives_with,
};
let mentions = {
let switchboard = sw_lock.read().unwrap();
switchboard.extract_mentions(mention_host)
};
insert_mentions(&mentions, pool).await?;
}
if old_contact.text_body != text_body {
sqlx::query!(
"delete from mentions where entity_id = $1 and entity_type = $2",
contact_id,
MentionHostType::ContactTextBody as DbId
)
.execute(pool)
.await?;
if text_body.is_some() {
let mention_host = MentionHost {
entity_id: contact_id,
entity_type: MentionHostType::ContactTextBody as DbId,
input: &text_body.unwrap(),
};
let mentions = {
let switchboard = sw_lock.read().unwrap();
switchboard.extract_mentions(mention_host)
};
insert_mentions(&mentions, pool).await?;
}
}
// these blocks are not in functions because payload gets progressively
@ -508,7 +474,6 @@ mod put {
}
{
// recalculate all contact mentions and name trie if name-list changed
let new_names: Vec<String> = payload
.name
.unwrap_or(vec![])
@ -523,60 +488,25 @@ mod put {
let old_names: Vec<String> = old_names.into_iter().map(|(s,)| s).collect();
if old_names != new_names {
// delete and regen *all* mentions, not just the ones for the current
// contact, since changing *this* contact's names can change, *globally*,
// which names have n=1 and thus are eligible for mentioning
sqlx::query!(
"delete from mentions; delete from names where contact_id = $1",
"delete from names where contact_id = $1",
contact_id
)
.execute(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 !new_names.is_empty() {
for name in &new_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(
new_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()
QueryBuilder::new(
"insert into names (contact_id, sort, name) "
).push_values(new_names.iter().enumerate(), |mut b, (sort, name)| {
b
.push_bind(contact_id)
.push_bind(DbId::try_from(sort).unwrap())
.push_bind(name);
}).build()
.persistent(false)
.fetch_all(pool)
.execute(pool)
.await?;
{
let mut switchboard = sw_lock.write().unwrap();
for name in &old_names {
switchboard.remove(name);
}
for name in recalc_names {
switchboard.add_mentionable(name.0, format!("/contact/{}", name.1));
}
}
}
@ -595,7 +525,7 @@ mod put {
if new_groups != old_groups {
sqlx::query!(
"delete from mentions; delete from groups where contact_id = $1",
"delete from groups where contact_id = $1",
contact_id
)
.execute(pool)
@ -613,36 +543,83 @@ mod put {
.execute(pool)
.await?;
}
}
}
{
let mut switchboard = sw_lock.write().unwrap();
for name in &old_groups {
// TODO i think we care about group name vs contact name counts,
// otherwise this will cause a problem (or we want to disallow
// setting group names that are contact names or vice versa?)
switchboard.remove(name);
}
for group in &new_groups {
switchboard
.add_mentionable(group.clone(), format!("/group/{}", slugify(group)));
}
}
let regen_all_mentions = {
let trie = Switchboard::gen_trie(pool).await?;
let mut swb = sw_lock.write().unwrap();
swb.check_and_assign(trie)
};
let regen_lives_with = old_contact.lives_with != payload.lives_with;
let regen_text_body = old_contact.text_body != text_body;
if regen_all_mentions {
sqlx::query("delete from mentions").execute(pool).await?;
} else {
if regen_lives_with {
sqlx::query!(
"delete from mentions where entity_id = $1 and entity_type = $2",
contact_id,
MentionHostType::ContactLivesWith as DbId
)
.execute(pool)
.await?;
}
if new_names != old_names || new_groups != old_groups {
let journal_entries: Vec<JournalEntry> =
sqlx::query_as("select * from journal_entries")
.fetch_all(pool)
.await?;
for entry in journal_entries {
let mentions = {
let switchboard = sw_lock.read().unwrap();
switchboard.extract_mentions(&entry)
};
insert_mentions(&mentions, pool).await?;
}
if regen_text_body {
sqlx::query!(
"delete from mentions where entity_id = $1 and entity_type = $2",
contact_id,
MentionHostType::ContactTextBody as DbId
)
.execute(pool)
.await?;
}
}
if regen_all_mentions || regen_lives_with {
let mention_host = MentionHost {
entity_id: contact_id,
entity_type: MentionHostType::ContactLivesWith as DbId,
input: &payload.lives_with,
};
let mentions = {
let switchboard = sw_lock.read().unwrap();
switchboard.extract_mentions(mention_host)
};
insert_mentions(&mentions, pool).await?;
}
if regen_all_mentions || regen_text_body {
if text_body.is_some() {
let mention_host = MentionHost {
entity_id: contact_id,
entity_type: MentionHostType::ContactTextBody as DbId,
input: &text_body.unwrap(),
};
let mentions = {
let switchboard = sw_lock.read().unwrap();
switchboard.extract_mentions(mention_host)
};
insert_mentions(&mentions, pool).await?;
}
}
if regen_all_mentions {
let journal_entries: Vec<JournalEntry> =
sqlx::query_as("select * from journal_entries")
.fetch_all(pool)
.await?;
for entry in journal_entries {
let mentions = {
let switchboard = sw_lock.read().unwrap();
switchboard.extract_mentions(&entry)
};
insert_mentions(&mentions, pool).await?;
}
}