Skip to main content

tilo_app/framework/
people.rs

1//! Who the participants are, by name: what a view or surface shows for an
2//! id an action recorded ([`ActionCx::author`](tilo_resource::ActionCx::author))
3//! or the participant reading ([`RecordEnv::me`](super::RecordEnv::me)).
4//!
5//! The document records ids — the same on every replica, whatever anyone is
6//! called — and the host knows the names: the room's roster announces them
7//! with the people who are present. A view resolves an id when it shows it,
8//! so a renamed person, or one whose name arrived after their edit, reads
9//! correctly everywhere at once.
10
11use std::cell::RefCell;
12use std::collections::HashMap;
13use std::rc::Rc;
14
15use ui_core::LocalState;
16use ui_core::permissions::UserId;
17use ui_core::presence::PresenceCtx;
18
19/// The names the host knows for the participants of the window's room.
20/// Cloneable; clones read the same roster.
21#[derive(Clone)]
22pub struct People {
23    roster: Rc<RefCell<Roster>>,
24    revision: LocalState<u64>,
25    /// The participant this window is for, when someone edits it.
26    me: Option<Rc<str>>,
27}
28
29#[derive(Default, PartialEq)]
30struct Roster {
31    names: HashMap<String, String>,
32    agents: Vec<String>,
33    /// The name this session's participant goes by, when the room said.
34    own: Option<String>,
35}
36
37/// What an id reads as when the participant is this window's own and the
38/// host has not said their name.
39const YOU: &str = "You";
40
41impl People {
42    pub(crate) fn new(me: Option<&str>) -> Self {
43        Self {
44            roster: Rc::default(),
45            revision: LocalState::new(0),
46            me: me.map(Rc::from),
47        }
48    }
49
50    /// The name to show for `id`: the roster's, the window's own participant
51    /// as "You" until the room names them, else the id itself — what a
52    /// record written before ids were names holds, and what a capture's
53    /// fixture people are called.
54    pub fn name(&self, id: &str) -> String {
55        let roster = self.roster.borrow();
56        if let Some(name) = roster.names.get(id) {
57            return if roster.agents.iter().any(|agent| agent == id) {
58                ui_core::history_source::agent_display_label(name)
59            } else {
60                name.clone()
61            };
62        }
63        if self.me.as_deref() == Some(id) {
64            return roster.own.clone().unwrap_or_else(|| YOU.to_owned());
65        }
66        id.to_owned()
67    }
68
69    /// Whether `id` is the participant this window is for.
70    pub fn is_me(&self, id: &str) -> bool {
71        self.me.as_deref() == Some(id)
72    }
73
74    /// Increases whenever a name changes, for a view to show names that
75    /// follow the roster (`Text::from_state(&people.revision(), ..)`).
76    pub fn revision(&self) -> LocalState<u64> {
77        self.revision.clone()
78    }
79
80    /// Take the names the host's roster holds now. Whether any changed.
81    pub(crate) fn follow(&self, presence: &PresenceCtx) -> bool {
82        let next = Roster {
83            names: presence
84                .names
85                .iter()
86                .map(|(UserId(id), name)| (id.clone(), name.clone()))
87                .collect(),
88            agents: presence
89                .names
90                .keys()
91                .filter(|user| presence.is_agent(user))
92                .map(|UserId(id)| id.clone())
93                .collect(),
94            own: presence.self_name().map(str::to_owned),
95        };
96        if *self.roster.borrow() == next {
97            return false;
98        }
99        *self.roster.borrow_mut() = next;
100        self.revision.set(self.revision.get().wrapping_add(1));
101        true
102    }
103
104    /// People named as `names` gives them, for a capture whose fixture
105    /// places collaborators, or a test.
106    #[cfg(any(test, feature = "test-support"))]
107    #[must_use]
108    pub fn named(me: Option<&str>, names: &[(&str, &str)]) -> Self {
109        let people = Self::new(me);
110        people.roster.borrow_mut().names = names
111            .iter()
112            .map(|(id, name)| ((*id).to_owned(), (*name).to_owned()))
113            .collect();
114        people
115    }
116}
117
118impl std::fmt::Debug for People {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("People")
121            .field("me", &self.me)
122            .field("known", &self.roster.borrow().names.len())
123            .field("revision", &self.revision.get())
124            .finish()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn an_id_reads_as_the_rosters_name_you_or_itself() {
134        let people = People::new(Some("me-id"));
135        assert_eq!(people.name("me-id"), "You");
136        assert_eq!(
137            people.name("Alice"),
138            "Alice",
139            "an unknown id reads as itself"
140        );
141
142        let mut presence = PresenceCtx::default();
143        presence.set_display_name(UserId("peer-id".into()), "Bob");
144        presence.set_self_participant(UserId("me-id".into()), "Ada");
145        assert!(people.follow(&presence));
146        assert!(
147            !people.follow(&presence),
148            "an unchanged roster changes nothing"
149        );
150        assert_eq!(people.name("peer-id"), "Bob");
151        assert_eq!(people.name("me-id"), "Ada");
152        assert!(people.is_me("me-id"));
153        assert_eq!(people.revision().get(), 1);
154    }
155}