use super::*;
use crate::state::ContactIdentitySnapshot;
impl AppCore {
fn is_contact(&self, owner: &str) -> bool {
PublicKey::from_hex(owner).is_ok()
&& self
.logged_in
.as_ref()
.is_some_and(|login| login.owner_pubkey.to_hex() != owner)
&& self.threads.contains_key(owner)
}
pub(super) fn remember_contact_name(&mut self, owner: &str) {
if !self.is_contact(owner) {
return;
}
if let Some(profile) = self.owner_profiles.get_mut(owner) {
if let Some(name) = profile.profile_label() {
profile.contact_memory.observe_name(&name);
}
}
}
pub(super) fn contact_identity(&self, owner: &str) -> Option<ContactIdentitySnapshot> {
if !self.is_contact(owner) {
return None;
}
let profile = self.owner_profiles.get(owner).cloned().unwrap_or_default();
let pending_name = profile.contact_memory.pending_name(profile.profile_label());
Some(ContactIdentitySnapshot {
is_following: self
.social_graph
.as_ref()
.is_some_and(|graph| graph.is_following(graph.get_root(), owner)),
can_follow: self
.logged_in
.as_ref()
.is_some_and(|login| login.owner_keys.is_some() && !login.relay_urls.is_empty()),
updating_follow: self.pending_follow.is_some(),
first_seen_name: profile.contact_memory.first_seen_name,
saved_name: profile.contact_memory.accepted_name,
pending_name,
is_favorite: profile.contact_memory.favorite,
})
}
pub(super) fn approve_contact_name(&mut self, owner: &str, name: &str) {
let Some(identity) = self.contact_identity(owner) else {
return;
};
if identity.pending_name.as_deref() != Some(name) {
return;
}
let now = unix_now().get();
let Some(mut memory) = self
.owner_profiles
.get(owner)
.map(|profile| profile.contact_memory.clone())
else {
return;
};
let Some(change) = memory.approve_name(name, name, now) else {
return;
};
if let Err(error) = self.write_name_approval(
owner,
memory
.first_seen_name
.unwrap_or_else(|| change.previous_name.clone()),
change.clone(),
false,
) {
self.push_debug_log("name_approval.save_failed", error.to_string());
self.state.toast = Some("Could not save name change. Try again.".into());
self.emit_state();
return;
}
self.push_system_notice(
owner,
format!(
"Name change approved: {} → {}",
change.previous_name, change.accepted_name
),
now,
);
self.mark_mobile_push_dirty();
self.bump_user_discovery_revision();
self.rebuild_persist_and_emit_state();
}
pub(super) fn set_contact_favorite(&mut self, owner: &str, favorite: bool) {
if !self.is_contact(owner) {
return;
}
let profile = self.owner_profiles.entry(owner.to_string()).or_default();
if profile.contact_memory.favorite == favorite {
return;
}
if !self.edit_private_contact_fields(
owner,
BTreeMap::from([("favorite".into(), serde_json::json!(favorite))]),
) {
return;
}
self.rebuild_persist_and_emit_state();
}
}
pub(super) fn contact_identity_from_db(
state: &AppState,
shared: Option<&SharedConnection>,
thread: Option<&ChatThreadSnapshot>,
) -> Option<ContactIdentitySnapshot> {
use rusqlite::OptionalExtension;
let thread = thread.filter(|thread| matches!(thread.kind, ChatKind::Direct))?;
let account = state.account.as_ref()?;
let owner = &thread.chat_id;
if account.public_key_hex == *owner || PublicKey::from_hex(owner).is_err() {
return None;
}
let conn = shared?.try_lock().ok()?;
let raw = conn
.query_row(
"SELECT contact_memory_json FROM owner_profiles WHERE owner_pubkey_hex = ?1",
[owner],
|row| row.get::<_, String>(0),
)
.optional()
.ok()?
.unwrap_or_else(|| "{}".into());
let memory: crate::contact_memory::ContactMemory = serde_json::from_str(&raw).ok()?;
let is_following = conn.query_row(
"SELECT EXISTS(SELECT 1 FROM user_discovery_users u JOIN user_discovery_state s ON s.id = 1
WHERE u.owner_pubkey_hex = ?1 AND s.owner_pubkey_hex = ?2)",
rusqlite::params![owner, account.public_key_hex], |row| row.get(0),
).ok()?;
Some(ContactIdentitySnapshot {
is_following,
can_follow: account.has_owner_signing_authority
&& !state.preferences.nostr_relay_urls.is_empty(),
updating_follow: false,
pending_name: memory.pending_name(thread.profile_name.clone()),
first_seen_name: memory.first_seen_name,
saved_name: memory.accepted_name,
is_favorite: memory.favorite,
})
}