iris_chat_core/
person_name.rs1use crate::core::{fallback_profile_name_for_identity, profile_name_for_identity};
2use nostr::PublicKey;
3
4#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
5pub struct PersonNamePresentation {
6 pub name: String,
7 pub is_fallback: bool,
8}
9
10#[uniffi::export]
13pub fn present_person_name(
14 display_name: String,
15 identity: String,
16 explicit_name: Option<String>,
17) -> PersonNamePresentation {
18 let label = display_name.trim();
19 let identity = identity.trim();
20 let generated = fallback_profile_name_for_identity(identity);
21 let explicit = !label.is_empty() && explicit_name.as_deref().map(str::trim) == Some(label);
22 let normalized = PublicKey::parse(identity).ok().map(|key| key.to_hex());
23 let is_fallback = !explicit
24 && (label.is_empty()
25 || (!identity.is_empty()
26 && (profile_name_for_identity(label, identity).is_none()
27 || label == generated
28 || label == legacy_generated_name(identity)
29 || normalized
30 .as_deref()
31 .is_some_and(|key| label == legacy_generated_name(key)))));
32 PersonNamePresentation {
33 name: if is_fallback { generated } else { label.into() },
34 is_fallback,
35 }
36}
37
38fn legacy_generated_name(identity: &str) -> String {
41 const ADJECTIVES: [&str; 12] = [
42 "Amber", "Bright", "Calm", "Clear", "Golden", "Lunar", "Nova", "Quiet", "Silver", "Solar",
43 "Velvet", "Wild",
44 ];
45 const NOUNS: [&str; 12] = [
46 "Aurora", "Comet", "Echo", "Falcon", "Harbor", "Listener", "Otter", "Raven", "Signal",
47 "Sparrow", "Tide", "Voyager",
48 ];
49 if identity.is_empty() {
50 return "Quiet Listener".into();
51 }
52 let hash = identity.bytes().fold(0_u32, |hash, byte| {
53 hash.wrapping_mul(31).wrapping_add(u32::from(byte))
54 }) as usize;
55 let adjective = ADJECTIVES.get(hash % ADJECTIVES.len()).unwrap_or(&"Quiet");
56 let noun = NOUNS
57 .get((hash / ADJECTIVES.len()) % NOUNS.len())
58 .unwrap_or(&"Listener");
59 format!("{adjective} {noun}")
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use nostr::ToBech32;
66
67 #[test]
68 fn identifiers_and_old_generated_names_share_a_stable_animal_fallback() {
69 let owner = "ab".repeat(32);
70 let npub = PublicKey::parse(&owner).unwrap().to_bech32().unwrap();
71 for label in [
72 "",
73 " ",
74 &owner,
75 "abababab…abababab",
76 &npub,
77 "Golden Listener",
78 ] {
79 let value = present_person_name(label.into(), owner.clone(), None);
80 assert_eq!(value.name, "Golden Hare");
81 assert!(value.is_fallback);
82 }
83 assert_eq!(
84 present_person_name(String::new(), npub, None),
85 present_person_name(String::new(), owner, None)
86 );
87 }
88
89 #[test]
90 fn explicit_names_are_preserved_even_when_they_match_a_generated_name_or_id() {
91 let owner = "ab".repeat(32);
92 for name in ["Alice", "Mum", "Golden Hare", "Amber Fox", &owner] {
93 let value = present_person_name(name.into(), owner.clone(), Some(name.into()));
94 assert_eq!(value.name, name);
95 assert!(!value.is_fallback);
96 }
97 for name in ["Alice", "Amber Fox", "Note to Self"] {
98 assert!(!present_person_name(name.into(), owner.clone(), None).is_fallback);
99 }
100 assert_eq!(
101 present_person_name(String::new(), String::new(), None).name,
102 "Quiet Otter"
103 );
104 }
105}