Skip to main content

iris_chat_core/
person_name.rs

1use 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/// Display-only repair of cached identifiers and generated names. Explicit
11/// profile names and nicknames are distinguished by provenance, not word shape.
12#[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
38// Recognize only the exact old generated value for this identity. These labels
39// may still be cached by an existing app; no stored profile is rewritten.
40fn 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}