tuwunel_api_client/sync/v5/rooms/
heroes.rs1use std::cmp::Ordering;
2
3use futures::{Stream, StreamExt, future::join};
4use ruma::{
5 MxcUri, OwnedMxcUri, OwnedUserId, RoomId, UserId,
6 api::client::sync::sync_events::v5::{
7 DisplayName,
8 response::{Hero, Heroes},
9 },
10};
11use tuwunel_core::utils::{BoolExt, ReadyExt, TryFutureExtExt, stream::WidebandExt};
12use tuwunel_service::Services;
13
14const MAX_HEROES: usize = 5;
15
16#[tracing::instrument(name = "heroes", level = "trace", skip_all)]
17pub(super) async fn calculate_heroes(
18 services: &Services,
19 sender_user: &UserId,
20 room_id: &RoomId,
21 room_name: Option<&DisplayName>,
22 room_avatar: Option<&MxcUri>,
23) -> (Option<Heroes>, Option<DisplayName>, Option<OwnedMxcUri>) {
24 let heroes: Heroes = room_name
25 .is_none()
26 .then_async(|| hero_members(services, sender_user, room_id))
27 .await
28 .unwrap_or_default();
29
30 let hero_name = match heroes.len().cmp(&(1_usize)) {
31 | Ordering::Less => None,
32 | Ordering::Equal => Some(
33 heroes[0]
34 .name
35 .clone()
36 .unwrap_or_else(|| heroes[0].user_id.as_str().into()),
37 ),
38 | Ordering::Greater => {
39 let firsts = heroes[1..]
40 .iter()
41 .map(|h| {
42 h.name
43 .clone()
44 .unwrap_or_else(|| h.user_id.as_str().into())
45 })
46 .collect::<Vec<_>>()
47 .join(", ");
48
49 let last = heroes[0]
50 .name
51 .clone()
52 .unwrap_or_else(|| heroes[0].user_id.as_str().into());
53
54 Some(format!("{firsts} and {last}")).map(Into::into)
55 },
56 };
57
58 let heroes_avatar = (room_avatar.is_none() && room_name.is_none())
59 .then(|| {
60 heroes
61 .first()
62 .and_then(|hero| hero.avatar.clone())
63 })
64 .flatten();
65
66 (Some(heroes), hero_name, heroes_avatar)
67}
68
69async fn hero_members(services: &Services, sender_user: &UserId, room_id: &RoomId) -> Heroes {
70 let members = services
71 .state_cache
72 .room_members(room_id)
73 .ready_filter(|&member| member != sender_user)
74 .map(ToOwned::to_owned);
75
76 first_heroes(members, async |user_id| {
77 let content = services
78 .state_accessor
79 .get_member(room_id, &user_id)
80 .ok()
81 .await?;
82
83 let name = content
84 .displayname
85 .is_none()
86 .then_async(|| services.profile.displayname(&user_id).ok());
87
88 let avatar = content
89 .avatar_url
90 .is_none()
91 .then_async(|| services.profile.avatar_url(&user_id).ok());
92
93 let (name, avatar) = join(name, avatar).await;
94 let name = name
95 .unwrap_or(content.displayname)
96 .map(Into::into);
97
98 let hero = Hero {
99 user_id,
100 avatar: avatar.unwrap_or(content.avatar_url),
101 name,
102 };
103
104 Some(hero)
105 })
106 .collect()
107 .await
108}
109
110fn first_heroes<Members, Lookup, Found>(
115 members: Members,
116 lookup: Lookup,
117) -> impl Stream<Item = Hero> + Send
118where
119 Members: Stream<Item = OwnedUserId> + Send,
120 Lookup: Fn(OwnedUserId) -> Found + Send,
121 Found: Future<Output = Option<Hero>> + Send,
122{
123 members
124 .widen_filter_map(MAX_HEROES, lookup)
125 .take(MAX_HEROES)
126}
127
128#[cfg(test)]
129mod tests {
130 use ruma::user_id;
131 use tokio::task::yield_now;
132 use tuwunel_core::utils::IterStream;
133
134 use super::*;
135
136 #[tokio::test]
137 async fn first_heroes_keep_member_order_past_slow_and_failed_lookups() {
138 let members = [
139 user_id!("@a:example.com"),
140 user_id!("@b:example.com"),
141 user_id!("@c:example.com"),
142 user_id!("@d:example.com"),
143 user_id!("@e:example.com"),
144 user_id!("@f:example.com"),
145 user_id!("@g:example.com"),
146 ];
147
148 let [slow, failed, ..] = members;
149 let heroes: Vec<_> =
150 first_heroes(members.map(ToOwned::to_owned).stream(), async |user_id| {
151 if user_id == slow {
152 yield_now().await;
153 }
154
155 user_id
156 .ne(failed)
157 .then_some(Hero { user_id, avatar: None, name: None })
158 })
159 .map(|hero| hero.user_id)
160 .collect()
161 .await;
162
163 let expected = [members[0], members[2], members[3], members[4], members[5]];
164
165 assert_eq!(heroes, expected.map(ToOwned::to_owned));
166 }
167}