matrix_ui_serializable/user/
user_profile.rs1use crossbeam_queue::SegQueue;
6use matrix_sdk::{
7 room::RoomMember,
8 ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId, UserId},
9};
10use serde::Serialize;
11use std::{
12 cell::RefCell,
13 collections::{BTreeMap, btree_map::Entry},
14};
15use tokio::sync::oneshot;
16use tracing::warn;
17
18use crate::{MatrixRequest, commands::submit_async_request};
19
20thread_local! {
21 static USER_PROFILE_CACHE: RefCell<BTreeMap<OwnedUserId, UserProfileCacheEntry>> = const { RefCell::new(BTreeMap::new()) };
25}
26#[derive(Debug, Clone)]
27pub(crate) enum UserProfileCacheEntry {
28 Requested,
30 Loaded {
32 user_profile: UserProfile,
33 rooms: BTreeMap<OwnedRoomId, RoomMember>,
34 },
35}
36
37static PENDING_USER_PROFILE_UPDATES: SegQueue<UserProfileUpdate> = SegQueue::new();
39
40pub fn enqueue_user_profile_update(update: UserProfileUpdate) {
42 PENDING_USER_PROFILE_UPDATES.push(update);
43}
44
45pub enum UserProfileUpdate {
48 Full {
50 new_profile: UserProfile,
51 room_id: OwnedRoomId,
52 room_member: RoomMember,
53 },
54 RoomMemberOnly {
56 room_id: OwnedRoomId,
57 room_member: RoomMember,
58 },
59 UserProfileOnly(UserProfile),
61}
62impl UserProfileUpdate {
63 #[allow(unused)]
65 pub fn user_id(&self) -> &UserId {
66 match self {
67 UserProfileUpdate::Full { new_profile, .. } => &new_profile.user_id,
68 UserProfileUpdate::RoomMemberOnly { room_member, .. } => room_member.user_id(),
69 UserProfileUpdate::UserProfileOnly(profile) => &profile.user_id,
70 }
71 }
72
73 pub fn get_user_profile_from_update(&self) -> Option<&UserProfile> {
74 match self {
75 UserProfileUpdate::Full { new_profile, .. } => Some(new_profile),
76 UserProfileUpdate::RoomMemberOnly { .. } => None,
77 UserProfileUpdate::UserProfileOnly(profile) => Some(profile),
78 }
79 }
80
81 fn apply_to_cache(self, cache: &mut BTreeMap<OwnedUserId, UserProfileCacheEntry>) {
83 match self {
84 UserProfileUpdate::Full {
85 new_profile,
86 room_id,
87 room_member,
88 } => match cache.entry(new_profile.user_id.clone()) {
89 Entry::Occupied(mut entry) => match entry.get_mut() {
90 e @ UserProfileCacheEntry::Requested => {
91 *e = UserProfileCacheEntry::Loaded {
92 user_profile: new_profile,
93 rooms: {
94 let mut room_members_map = BTreeMap::new();
95 room_members_map.insert(room_id, room_member);
96 room_members_map
97 },
98 };
99 }
100 UserProfileCacheEntry::Loaded {
101 user_profile,
102 rooms,
103 } => {
104 *user_profile = new_profile;
105 rooms.insert(room_id, room_member);
106 }
107 },
108 Entry::Vacant(entry) => {
109 entry.insert(UserProfileCacheEntry::Loaded {
110 user_profile: new_profile,
111 rooms: {
112 let mut room_members_map = BTreeMap::new();
113 room_members_map.insert(room_id, room_member);
114 room_members_map
115 },
116 });
117 }
118 },
119 UserProfileUpdate::RoomMemberOnly {
120 room_id,
121 room_member,
122 } => {
123 match cache.entry(room_member.user_id().to_owned()) {
124 Entry::Occupied(mut entry) => match entry.get_mut() {
125 e @ UserProfileCacheEntry::Requested => {
126 warn!(
128 "BUG: User profile cache entry was `Requested` for user {} when handling RoomMemberOnly update",
129 room_member.user_id()
130 );
131 *e = UserProfileCacheEntry::Loaded {
132 user_profile: UserProfile {
133 user_id: room_member.user_id().to_owned(),
134 username: None,
135 avatar: room_member.avatar_url().map(|url| url.to_owned()),
136 },
137 rooms: {
138 let mut room_members_map = BTreeMap::new();
139 room_members_map.insert(room_id, room_member);
140 room_members_map
141 },
142 };
143 }
144 UserProfileCacheEntry::Loaded { rooms, .. } => {
145 rooms.insert(room_id, room_member);
146 }
147 },
148 Entry::Vacant(entry) => {
149 warn!(
151 "BUG: User profile cache entry not found for user {} when handling RoomMemberOnly update",
152 room_member.user_id()
153 );
154 entry.insert(UserProfileCacheEntry::Loaded {
155 user_profile: UserProfile {
156 user_id: room_member.user_id().to_owned(),
157 username: None,
158 avatar: room_member.avatar_url().map(|url| url.to_owned()),
159 },
160 rooms: {
161 let mut room_members_map = BTreeMap::new();
162 room_members_map.insert(room_id, room_member);
163 room_members_map
164 },
165 });
166 }
167 }
168 }
169 UserProfileUpdate::UserProfileOnly(new_profile) => {
170 match cache.entry(new_profile.user_id.clone()) {
171 Entry::Occupied(mut entry) => match entry.get_mut() {
172 e @ UserProfileCacheEntry::Requested => {
173 *e = UserProfileCacheEntry::Loaded {
174 user_profile: new_profile,
175 rooms: BTreeMap::new(),
176 };
177 }
178 UserProfileCacheEntry::Loaded { user_profile, .. } => {
179 *user_profile = new_profile;
180 }
181 },
182 Entry::Vacant(entry) => {
183 entry.insert(UserProfileCacheEntry::Loaded {
184 user_profile: new_profile,
185 rooms: BTreeMap::new(),
186 });
187 }
188 }
189 }
190 }
191 }
192}
193
194pub fn process_user_profile_updates() {
196 USER_PROFILE_CACHE.with_borrow_mut(|cache| {
197 while let Some(update) = PENDING_USER_PROFILE_UPDATES.pop() {
198 update.apply_to_cache(cache);
200 }
201 });
202}
203
204pub fn with_sender(
207 user_id: OwnedUserId,
208 room_id: Option<&OwnedRoomId>,
209 fetch_if_missing: bool,
210 sender: oneshot::Sender<Option<UserProfile>>,
211) {
212 USER_PROFILE_CACHE.with_borrow_mut(|cache| match cache.entry(user_id) {
213 Entry::Occupied(entry) => match entry.get() {
214 UserProfileCacheEntry::Loaded {
215 user_profile,
216 rooms,
217 } => {
218 if room_id.is_some_and(|id| !rooms.contains_key(id)) {
219 submit_async_request(MatrixRequest::GetUserProfile {
220 user_id: entry.key().clone(),
221 room_id: room_id.cloned(),
222 local_only: false,
223 sender: None,
224 });
225 }
226 let _ = sender.send(Some(user_profile.to_owned()));
227 }
228 UserProfileCacheEntry::Requested => {
229 }
231 },
232 Entry::Vacant(entry) => {
233 if fetch_if_missing {
234 submit_async_request(MatrixRequest::GetUserProfile {
237 user_id: entry.key().clone(),
238 room_id: room_id.cloned(),
239 local_only: false,
240 sender: Some(sender),
241 });
242 entry.insert(UserProfileCacheEntry::Requested);
243 }
244 }
245 })
246}
247
248pub enum CachedName {
250 FoundInRoom(Option<String>),
253 FoundInProfile(Option<String>),
256 NotFound,
258}
259impl CachedName {
260 pub fn was_found(&self) -> bool {
261 matches!(self, Self::FoundInRoom(_) | Self::FoundInProfile(_))
262 }
263
264 pub fn into_option(self) -> Option<String> {
265 self.into()
266 }
267
268 pub fn as_deref(&self) -> Option<&str> {
269 match self {
270 CachedName::FoundInRoom(name) | CachedName::FoundInProfile(name) => name.as_deref(),
271 CachedName::NotFound => None,
272 }
273 }
274}
275impl From<CachedName> for Option<String> {
276 fn from(cached_name: CachedName) -> Self {
277 match cached_name {
278 CachedName::FoundInRoom(name) => name,
279 CachedName::FoundInProfile(name) => name,
280 CachedName::NotFound => None,
281 }
282 }
283}
284
285pub fn _clear_user_profile_cache() {
287 USER_PROFILE_CACHE.with_borrow_mut(|cache| {
289 cache.clear();
290 });
291}
292
293#[derive(Clone, Debug, Serialize)]
295pub struct UserProfile {
296 pub user_id: OwnedUserId,
297 pub username: Option<String>,
301 pub avatar: Option<OwnedMxcUri>,
302}
303impl UserProfile {
304 pub fn displayable_name(&self) -> &str {
306 if let Some(un) = self.username.as_ref()
307 && !un.is_empty()
308 {
309 return un.as_str();
310 }
311 self.user_id.as_str()
312 }
313}