iris_chat_core/
contact_memory.rs1use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(default)]
11pub struct ContactMemory {
12 pub first_seen_name: Option<String>,
13 pub accepted_name: Option<String>,
14 pub favorite: bool,
15 pub name_changes: Vec<AcceptedNameChange>,
16}
17
18#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
19pub struct AcceptedNameChange {
20 pub previous_name: String,
21 pub accepted_name: String,
22 pub accepted_at_secs: u64,
23}
24
25impl ContactMemory {
26 pub fn observe_name(&mut self, name: &str) {
29 if name.trim().is_empty() {
30 return;
31 }
32 self.first_seen_name.get_or_insert_with(|| name.to_string());
33 self.accepted_name.get_or_insert_with(|| name.to_string());
34 }
35
36 pub fn pending_name(&self, current_name: Option<String>) -> Option<String> {
38 current_name.filter(|name| {
39 !name.trim().is_empty()
40 && self
41 .accepted_name
42 .as_ref()
43 .is_some_and(|saved| saved != name)
44 })
45 }
46
47 pub fn approve_name(
50 &mut self,
51 expected: &str,
52 current: &str,
53 now_secs: u64,
54 ) -> Option<AcceptedNameChange> {
55 if expected != current || expected.trim().is_empty() {
56 return None;
57 }
58 let previous = self
59 .accepted_name
60 .as_ref()
61 .filter(|name| *name != expected)?
62 .clone();
63 let change = AcceptedNameChange {
64 previous_name: previous,
65 accepted_name: expected.to_string(),
66 accepted_at_secs: now_secs,
67 };
68 self.accepted_name = Some(change.accepted_name.clone());
69 self.name_changes.push(change.clone());
70 Some(change)
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn remembers_first_real_name_and_keeps_profile_updates_pending() {
80 let mut memory = ContactMemory::default();
81 memory.observe_name(" ");
82 assert_eq!(memory, ContactMemory::default());
83 assert_eq!(memory.pending_name(Some("Alice".into())), None);
84 memory.observe_name("Alice");
85 memory.observe_name("Bob");
86 assert_eq!(memory.first_seen_name.as_deref(), Some("Alice"));
87 assert_eq!(memory.accepted_name.as_deref(), Some("Alice"));
88 assert_eq!(memory.pending_name(Some("Bob".into())), Some("Bob".into()));
89 assert_eq!(memory.pending_name(Some("Alice".into())), None);
90 assert_eq!(memory.pending_name(Some(" ".into())), None);
91 assert_eq!(memory.pending_name(None), None);
92 }
93
94 #[test]
95 fn requires_current_explicit_approval_and_retains_all_accepted_names() {
96 let mut memory = ContactMemory::default();
97 assert_eq!(memory.approve_name("Bob", "Bob", 100), None);
98 memory.observe_name("Alice");
99 assert_eq!(memory.approve_name("Bob", "Carol", 100), None);
100 assert_eq!(memory.approve_name(" ", " ", 100), None);
101 assert_eq!(memory.approve_name("Alice", "Alice", 100), None);
102 let first = memory.approve_name("Bob", "Bob", 100).unwrap();
103 let second = memory.approve_name("Alice", "Alice", 101).unwrap();
104 assert_eq!(memory.first_seen_name.as_deref(), Some("Alice"));
105 assert_eq!(memory.accepted_name.as_deref(), Some("Alice"));
106 assert_eq!(memory.name_changes, vec![first, second]);
107 let portable: ContactMemory =
108 serde_json::from_str(include_str!("../../test-fixtures/contact-memory.json")).unwrap();
109 assert_eq!(memory, portable);
110 let json = serde_json::to_string(&memory).unwrap();
111 assert_eq!(
112 serde_json::from_str::<ContactMemory>(&json).unwrap(),
113 memory
114 );
115 }
116
117 #[test]
118 fn favorites_are_independent_and_missing_fields_default_for_old_storage() {
119 let mut memory: ContactMemory = serde_json::from_str(r#"{"favorite":true}"#).unwrap();
120 memory.observe_name("Alice");
121 assert!(memory.favorite);
122 memory.favorite = false;
123 assert_eq!(memory.first_seen_name.as_deref(), Some("Alice"));
124 assert_eq!(memory.accepted_name.as_deref(), Some("Alice"));
125 assert!(memory.name_changes.is_empty());
126 }
127}