Skip to main content

matrix_ui_serializable/user/
user_profile.rs

1//! A cache of user profiles and room membership info, indexed by user ID.
2//!
3//! The cache is only accessible from the main UI thread.
4
5use 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    /// A cache of each user's profile and the rooms they are a member of, indexed by user ID.
22    ///
23    /// To be of any use, this cache must only be accessed by the main UI thread.
24    static USER_PROFILE_CACHE: RefCell<BTreeMap<OwnedUserId, UserProfileCacheEntry>> = const { RefCell::new(BTreeMap::new()) };
25}
26#[derive(Debug, Clone)]
27pub(crate) enum UserProfileCacheEntry {
28    /// A request has been issued and we're waiting for it to complete.
29    Requested,
30    /// The profile has been successfully loaded from the server.
31    Loaded {
32        user_profile: UserProfile,
33        rooms: BTreeMap<OwnedRoomId, RoomMember>,
34    },
35}
36
37/// The queue of user profile updates waiting to be processed by the UI thread's event handler.
38static PENDING_USER_PROFILE_UPDATES: SegQueue<UserProfileUpdate> = SegQueue::new();
39
40/// Enqueues a new user profile update and signals the UI that an update is available.
41pub fn enqueue_user_profile_update(update: UserProfileUpdate) {
42    PENDING_USER_PROFILE_UPDATES.push(update);
43}
44
45/// A user profile update, which can include changes to a user's full profile
46/// and/or room membership info.
47pub enum UserProfileUpdate {
48    /// A fully-fetched user profile, with info about the user's membership in a given room.
49    Full {
50        new_profile: UserProfile,
51        room_id: OwnedRoomId,
52        room_member: RoomMember,
53    },
54    /// An update to the user's room membership info only, without any profile changes.
55    RoomMemberOnly {
56        room_id: OwnedRoomId,
57        room_member: RoomMember,
58    },
59    /// An update to the user's profile only, without changes to room membership info.
60    UserProfileOnly(UserProfile),
61}
62impl UserProfileUpdate {
63    /// Returns the user ID associated with this update.
64    #[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    /// Applies this update to the given user profile info cache.
82    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                            // This shouldn't happen, but we can still technically handle it correctly.
127                            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                        // This shouldn't happen, but we can still technically handle it correctly.
150                        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
194/// Processes all pending user profile updates in the queue.
195pub fn process_user_profile_updates() {
196    USER_PROFILE_CACHE.with_borrow_mut(|cache| {
197        while let Some(update) = PENDING_USER_PROFILE_UPDATES.pop() {
198            // Insert the updated info into the cache
199            update.apply_to_cache(cache);
200        }
201    });
202}
203
204/// Invokes the given closure with cached user profile info for the given user ID
205/// (optionally in the given room) if it exists in the cache, otherwise does nothing.
206pub 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                // log!("User {} profile request is already in flight....", entry.key());
230            }
231        },
232        Entry::Vacant(entry) => {
233            if fetch_if_missing {
234                // log!("Did not find User {} in cache, fetching from server.", entry.key());
235                // TODO: use the extra `via` parameters from `matrix_to_uri.via()`.
236                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
248/// A user's display name in our cache.
249pub enum CachedName {
250    /// The user's display name was found for the specified room (most accurate).
251    /// If `None`, they did not set a display name for that room.
252    FoundInRoom(Option<String>),
253    /// The user's display name was found in their general account profile.
254    /// If `None`, they have not set a display name at all.
255    FoundInProfile(Option<String>),
256    /// No info about the user was found in the cache.
257    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
285/// Clears cached user profile.
286pub fn _clear_user_profile_cache() {
287    // Clear user profile cache
288    USER_PROFILE_CACHE.with_borrow_mut(|cache| {
289        cache.clear();
290    });
291}
292
293/// Information retrieved about a user: their displayable name, ID, and known avatar state.
294#[derive(Clone, Debug, Serialize)]
295pub struct UserProfile {
296    pub user_id: OwnedUserId,
297    /// The user's default display name, if set.
298    /// Note that a user may have per-room display names,
299    /// so this should be considered a fallback.
300    pub username: Option<String>,
301    pub avatar: Option<OwnedMxcUri>,
302}
303impl UserProfile {
304    /// Returns the user's displayable name, using the user ID as a fallback.
305    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}