1use std::cell::RefCell;
15use std::collections::HashMap;
16use std::rc::Rc;
17
18use tilo_resource::{ActionKey, Binds, Resource};
19use ui_core::LocalState;
20use ui_core::icons::Icon;
21use ui_core::presence::{ParticipantKind, PresenceCtx};
22use ui_core::widget::mention_picker::{MentionCandidate, merged_roster};
23use ui_core::widget::{Button, ButtonRole, Element, Stack, Tone};
24
25use super::ViewCx;
26
27#[derive(Clone)]
30pub struct People {
31 roster: Rc<RefCell<Roster>>,
32 revision: LocalState<u64>,
33 me: Option<Rc<str>>,
35 local: Option<Rc<str>>,
38}
39
40#[derive(Default, PartialEq)]
41struct Roster {
42 names: HashMap<String, String>,
44 own: Option<String>,
46 members: Vec<MentionCandidate>,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct Actor {
56 pub id: String,
59 pub name: String,
61 pub agent: bool,
63}
64
65impl People {
66 pub(crate) fn new(me: Option<&str>) -> Self {
67 Self {
68 roster: Rc::default(),
69 revision: LocalState::new(0),
70 local: me.and_then(|_| {
71 ui_core::local_identity::bound()
72 .map(|identity| identity.display_name.trim().to_owned())
73 .filter(|name| !name.is_empty())
74 .map(Rc::from)
75 }),
76 me: me.map(Rc::from),
77 }
78 }
79
80 pub fn name(&self, id: &str) -> String {
85 let roster = self.roster.borrow();
86 if let Some(name) = roster.names.get(id) {
87 return name.clone();
88 }
89 if self.me.as_deref() == Some(id)
90 && let Some(own) = roster.own.as_deref().or(self.local.as_deref())
91 {
92 return own.to_owned();
93 }
94 id.to_owned()
95 }
96
97 pub fn members(&self) -> Vec<MentionCandidate> {
101 self.roster.borrow().members.clone()
102 }
103
104 pub fn actor(&self, id: &str) -> Actor {
110 let (identity, bare, agent) = if let Some(bare) = id.strip_prefix("person:") {
111 (bare, bare, false)
112 } else if let Some(bare) = id.strip_prefix("agent:") {
113 (id, bare, true)
114 } else {
115 (id, id, false)
116 };
117 let name = [identity, bare]
118 .into_iter()
119 .map(|key| self.name(key))
120 .find(|name| name != identity && name != bare)
121 .unwrap_or_else(|| bare.to_owned());
122 Actor {
123 id: identity.to_owned(),
124 name,
125 agent,
126 }
127 }
128
129 pub fn others_here(&self) -> bool {
131 self.roster.borrow().members.iter().any(|member| {
132 !member.is_self && member.kind == ui_core::presence::ParticipantKind::Person
133 })
134 }
135
136 pub fn is_me(&self, id: &str) -> bool {
138 self.me.as_deref() == Some(id)
139 }
140
141 pub fn revision(&self) -> LocalState<u64> {
144 self.revision.clone()
145 }
146
147 pub(crate) fn follow(&self, presence: &PresenceCtx) -> bool {
149 let next = Roster {
150 names: presence
151 .names
152 .keys()
153 .map(|user| (user.0.clone(), presence.display_name(user)))
154 .collect(),
155 own: presence.self_label(),
156 members: merged_roster(presence, &[]),
157 };
158 if *self.roster.borrow() == next {
159 return false;
160 }
161 *self.roster.borrow_mut() = next;
162 self.revision.set(self.revision.get().wrapping_add(1));
163 true
164 }
165
166 #[cfg(any(test, feature = "test-support"))]
169 #[must_use]
170 pub fn named(me: Option<&str>, names: &[(&str, &str)]) -> Self {
171 let people = Self::new(me);
172 people.roster.borrow_mut().names = names
173 .iter()
174 .map(|(id, name)| ((*id).to_owned(), (*name).to_owned()))
175 .collect();
176 people
177 }
178}
179
180impl<R: Resource, T: 'static> ViewCx<R, T> {
181 pub fn person_picker<B: Binds<R, Option<String>> + Clone>(
191 &self,
192 id: &'static str,
193 label: String,
194 nobody: String,
195 value_of: impl Fn(&MentionCandidate) -> Option<String> + 'static,
196 read: impl Fn(&T) -> Option<String> + 'static,
197 action: B,
198 ) -> Element {
199 let people = self.people();
200 let revision = people.revision();
201 let label = Rc::new(label);
202 let nobody = Rc::new(nobody);
203 let value_of = Rc::new(value_of);
204 let read = Rc::new(read);
205 self.switch(&revision, move |cx, _| {
206 let members = people.members();
207 let held = {
208 let read = Rc::clone(&read);
209 cx.value(move |state: &T| read(state))
210 };
211 let people = people.clone();
212 let (label, nobody, value_of) =
213 (Rc::clone(&label), Rc::clone(&nobody), Rc::clone(&value_of));
214 let held_action = action.clone();
215 cx.switch(&held, move |cx, held| {
216 let who = held.as_deref().map(|value| people.actor(value));
217 let mut choices = Stack::new();
218 choices = choices.push(cx.person_choice(
219 &format!("{id}-none"),
220 &nobody,
221 None,
222 None,
223 false,
224 held_action.clone(),
225 ));
226 for member in &members {
227 let Some(value) = value_of(member) else {
228 continue;
229 };
230 choices = choices.push(cx.person_choice(
231 &format!("{id}-{}", member.id),
232 &member.name,
233 Some(value),
234 Some((&member.name, &member.id)),
235 member.kind == ParticipantKind::Agent,
236 held_action.clone(),
237 ));
238 }
239 let (name, agent, chip) = match &who {
240 Some(who) => (
241 who.name.clone(),
242 who.agent,
243 Button::new(id, &who.name)
244 .with_avatar(&who.name, &who.id)
245 .showing_choice(),
246 ),
247 None => (
248 nobody.to_string(),
249 false,
250 Button::new(id, nobody.as_str())
251 .with_icon(Icon::User)
252 .with_placeholder()
253 .showing_choice(),
254 ),
255 };
256 let chip = chip.with_accessible_name(&format!("{label}: {name}"));
257 let chip = if agent { chip.tone(Tone::Info) } else { chip };
258 Box::new(chip.with_popover(choices)) as Element
259 })
260 })
261 }
262
263 fn person_choice<B: Binds<R, Option<String>>>(
266 &self,
267 id: &str,
268 label: &str,
269 value: Option<String>,
270 face: Option<(&str, &str)>,
271 agent: bool,
272 action: B,
273 ) -> Element {
274 let mut button = Button::new(id, label).role(ButtonRole::Plain);
275 button = match face {
276 Some((name, identity)) => button.with_avatar(name, identity),
277 None => button.with_icon(Icon::User).with_placeholder(),
278 };
279 if agent {
280 button = button.tone(Tone::Info);
281 }
282 let binding = self.actions().binding(Chosen {
283 binds: action,
284 value,
285 });
286 match binding {
287 Some(binding) => Box::new(button.on_action(binding)),
288 None => Box::new(button.with_enabled(false)),
289 }
290 }
291}
292
293struct Chosen<B> {
296 binds: B,
297 value: Option<String>,
298}
299
300impl<R: Resource, B: Binds<R, Option<String>>> Binds<R, ()> for Chosen<B> {
301 fn action_id(&self) -> &'static str {
302 self.binds.action_id()
303 }
304
305 fn action_key(&self) -> ActionKey {
306 self.binds.action_key()
307 }
308
309 fn arguments(&self, (): ()) -> Result<serde_json::Map<String, serde_json::Value>, String> {
310 self.binds.arguments(self.value.clone())
311 }
312}
313
314impl std::fmt::Debug for People {
315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316 f.debug_struct("People")
317 .field("me", &self.me)
318 .field("local", &self.local)
319 .field("known", &self.roster.borrow().names.len())
320 .field("revision", &self.revision.get())
321 .finish()
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use ui_core::permissions::UserId;
329
330 #[test]
331 fn an_id_reads_as_the_rosters_name_or_itself() {
332 let people = People::new(Some("me-id"));
333 assert_eq!(
334 people.name("me-id"),
335 "me-id",
336 "nobody is called \"You\": an unnamed self reads as its id"
337 );
338 assert_eq!(
339 people.name("Alice"),
340 "Alice",
341 "an unknown id reads as itself"
342 );
343
344 let mut presence = PresenceCtx::default();
345 presence.set_display_name(UserId("peer-id".into()), "Bob");
346 presence.set_self_participant(UserId("me-id".into()), "Ada");
347 assert!(people.follow(&presence));
348 assert!(
349 !people.follow(&presence),
350 "an unchanged roster changes nothing"
351 );
352 assert_eq!(people.name("peer-id"), "Bob");
353 assert_eq!(people.name("me-id"), "Ada");
354 assert!(people.is_me("me-id"));
355 assert_eq!(people.revision().get(), 1);
356
357 presence.set_display_name(UserId("peer-id".into()), "ada");
358 assert!(people.follow(&presence));
359 assert_eq!(people.name("me-id"), "Ada (1)");
360 assert_eq!(people.name("peer-id"), "ada (2)");
361 }
362
363 #[test]
364 fn an_actors_record_spelling_reads_as_the_name_the_workspace_gives() {
365 let people = People::new(None);
366 let mut presence = PresenceCtx::default();
367 presence.set_display_name(UserId("bob".into()), "Bob");
368 presence.set_display_name(UserId("agent:scribe".into()), "Scribe");
369 presence.set_participant_kind(UserId("agent:scribe".into()), ParticipantKind::Agent);
370 assert!(people.follow(&presence));
371
372 assert_eq!(
373 people.actor("person:bob"),
374 Actor {
375 id: "bob".into(),
376 name: "Bob".into(),
377 agent: false,
378 },
379 "an assignee's spelling is not a name"
380 );
381 assert_eq!(
382 people.actor("agent:scribe"),
383 Actor {
384 id: "agent:scribe".into(),
385 name: "Scribe · Agent".into(),
386 agent: true,
387 },
388 "an agent keeps the id presence colours its face by"
389 );
390 assert_eq!(
391 people.actor("person:stranger"),
392 Actor {
393 id: "stranger".into(),
394 name: "stranger".into(),
395 agent: false,
396 },
397 "an id the workspace names not reads without its prefix"
398 );
399 assert_eq!(people.actor("bob").name, "Bob", "a bare id reads the same");
400 }
401}