matrix_ui_serializable/user/
user_profile.rs

1use std::{
2    collections::{BTreeMap, BTreeSet, btree_map::Entry},
3    sync::{Arc, LazyLock},
4};
5
6use crossbeam_queue::SegQueue;
7use matrix_sdk::{
8    room::RoomMember,
9    ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId},
10};
11use serde::Serialize;
12use tokio::sync::RwLock;
13
14use crate::models::{
15    async_requests::{MatrixRequest, submit_async_request},
16    state_updater::StateUpdater,
17};
18
19use crate::init::singletons::{UIUpdateMessage, broadcast_event};
20
21/// Information retrieved about a user: their displayable name, ID, and known avatar state.
22#[derive(Debug, Clone, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct UserProfile {
25    pub user_id: OwnedUserId,
26    /// The user's default display name, if set.
27    /// Note that a user may have per-room display names,
28    /// so this should be considered a fallback.
29    pub username: Option<String>,
30    pub avatar_url: Option<OwnedMxcUri>,
31}
32impl UserProfile {
33    /// Returns the user's displayable name, using the user ID as a fallback.
34    pub fn _displayable_name(&self) -> &str {
35        if let Some(un) = self.username.as_ref() {
36            if !un.is_empty() {
37                return un.as_str();
38            }
39        }
40        self.user_id.as_str()
41    }
42}
43
44/// The queue of user profile updates waiting to be processed by the UI thread's event handler.
45static PENDING_USER_PROFILE_UPDATES: SegQueue<UserProfileUpdate> = SegQueue::new();
46
47/// Enqueues a new user profile update and signals the UI that an update is available.
48pub fn enqueue_user_profile_update(update: UserProfileUpdate) {
49    PENDING_USER_PROFILE_UPDATES.push(update);
50    broadcast_event(UIUpdateMessage::RefreshUI).expect("Couldn't broadcast event to UI");
51}
52
53/// A user profile update, which can include changes to a user's full profile
54/// and/or room membership info.
55pub enum UserProfileUpdate {
56    /// A fully-fetched user profile, with info about the user's membership in a given room.
57    Full {
58        new_profile: UserProfile,
59        room_id: OwnedRoomId,
60        _room_member: RoomMember,
61    },
62    /// An update to the user's room membership info only, without any profile changes.
63    RoomMemberOnly {
64        room_id: OwnedRoomId,
65        room_member: RoomMember,
66    },
67    /// An update to the user's profile only, without changes to room membership info.
68    UserProfileOnly(UserProfile),
69}
70
71impl UserProfileUpdate {
72    /// Applies this update to the given user profile info cache.
73    fn apply_to_cache(self, cache: &mut BTreeMap<OwnedUserId, UserProfileCacheEntry>) {
74        match self {
75            UserProfileUpdate::Full {
76                new_profile,
77                room_id,
78                _room_member: _,
79            } => match cache.entry(new_profile.user_id.clone()) {
80                Entry::Occupied(mut entry) => match entry.get_mut() {
81                    e @ UserProfileCacheEntry::Requested => {
82                        *e = UserProfileCacheEntry::Loaded {
83                            user_profile: new_profile,
84                            rooms: {
85                                let mut rooms = BTreeSet::new();
86                                rooms.insert(room_id);
87                                rooms
88                            },
89                        };
90                    }
91                    UserProfileCacheEntry::Loaded {
92                        user_profile,
93                        rooms,
94                    } => {
95                        *user_profile = new_profile;
96                        rooms.insert(room_id);
97                    }
98                },
99                Entry::Vacant(entry) => {
100                    entry.insert(UserProfileCacheEntry::Loaded {
101                        user_profile: new_profile,
102                        rooms: {
103                            let mut rooms = BTreeSet::new();
104                            rooms.insert(room_id);
105                            rooms
106                        },
107                    });
108                }
109            },
110            UserProfileUpdate::RoomMemberOnly {
111                room_id,
112                room_member,
113            } => {
114                match cache.entry(room_member.user_id().to_owned()) {
115                    Entry::Occupied(mut entry) => match entry.get_mut() {
116                        e @ UserProfileCacheEntry::Requested => {
117                            // This shouldn't happen, but we can still technically handle it correctly.
118                            eprintln!(
119                                "BUG: User profile cache entry was `Requested` for user {} when handling RoomMemberOnly update",
120                                room_member.user_id()
121                            );
122                            *e = UserProfileCacheEntry::Loaded {
123                                user_profile: UserProfile {
124                                    user_id: room_member.user_id().to_owned(),
125                                    username: None,
126                                    avatar_url: room_member.avatar_url().map(|url| url.to_owned()),
127                                },
128                                rooms: {
129                                    let mut rooms = BTreeSet::new();
130                                    rooms.insert(room_id);
131                                    rooms
132                                },
133                            };
134                        }
135                        UserProfileCacheEntry::Loaded { rooms, .. } => {
136                            rooms.insert(room_id);
137                        }
138                    },
139                    Entry::Vacant(entry) => {
140                        // This shouldn't happen, but we can still technically handle it correctly.
141                        eprintln!(
142                            "BUG: User profile cache entry not found for user {} when handling RoomMemberOnly update",
143                            room_member.user_id()
144                        );
145                        entry.insert(UserProfileCacheEntry::Loaded {
146                            user_profile: UserProfile {
147                                user_id: room_member.user_id().to_owned(),
148                                username: None,
149                                avatar_url: room_member.avatar_url().map(|url| url.to_owned()),
150                            },
151                            rooms: {
152                                let mut rooms = BTreeSet::new();
153                                rooms.insert(room_id);
154                                rooms
155                            },
156                        });
157                    }
158                }
159            }
160            UserProfileUpdate::UserProfileOnly(new_profile) => {
161                match cache.entry(new_profile.user_id.clone()) {
162                    Entry::Occupied(mut entry) => match entry.get_mut() {
163                        e @ UserProfileCacheEntry::Requested => {
164                            *e = UserProfileCacheEntry::Loaded {
165                                user_profile: new_profile,
166                                rooms: BTreeSet::new(),
167                            };
168                        }
169                        UserProfileCacheEntry::Loaded { user_profile, .. } => {
170                            *user_profile = new_profile;
171                        }
172                    },
173                    Entry::Vacant(entry) => {
174                        entry.insert(UserProfileCacheEntry::Loaded {
175                            user_profile: new_profile,
176                            rooms: BTreeSet::new(),
177                        });
178                    }
179                }
180            }
181        }
182    }
183}
184
185/// A cache of each user's profile and the rooms they are a member of, indexed by user ID.
186#[derive(Debug, Clone, Serialize)]
187pub struct UserProfileMap(BTreeMap<OwnedUserId, UserProfileCacheEntry>);
188
189/// A cache of each user's profile and the rooms they are a member of, indexed by user ID.
190static USER_PROFILE_CACHE: LazyLock<RwLock<UserProfileMap>> =
191    LazyLock::new(|| RwLock::new(UserProfileMap(BTreeMap::new())));
192
193/// Processes all pending user profile updates in the queue.
194pub async fn process_user_profile_updates(updaters: &Arc<Box<dyn StateUpdater>>) -> bool {
195    let mut updated = false;
196    if PENDING_USER_PROFILE_UPDATES.is_empty() {
197        return updated; // Return early if the queue is empty to avoid acquiring the lock.
198    };
199    {
200        let mut lock = USER_PROFILE_CACHE.write().await;
201        while let Some(update) = PENDING_USER_PROFILE_UPDATES.pop() {
202            // Insert the updated info into the cache
203            update.apply_to_cache(&mut lock.0);
204            updated = true;
205        }
206    } // We drop the write lock here
207    if updated {
208        let lock = USER_PROFILE_CACHE.read().await;
209        updaters
210            .update_profile(&lock)
211            .expect("Couldn't update profiles frontend state");
212    }
213    updated
214}
215
216/// Submit a request to retrieve the user profile or returns true if the entry is already requested.
217pub async fn fetch_user_profile(user_id: OwnedUserId, room_id: Option<OwnedRoomId>) -> bool {
218    let mut lock = USER_PROFILE_CACHE.write().await;
219    match lock.0.entry(user_id) {
220        Entry::Occupied(_) => true,
221        Entry::Vacant(entry) => {
222            submit_async_request(MatrixRequest::GetUserProfile {
223                user_id: entry.key().clone(),
224                room_id,
225                local_only: false,
226            });
227            entry.insert(UserProfileCacheEntry::Requested);
228            false
229        }
230    }
231}
232
233#[derive(Debug, Clone, Serialize)]
234#[serde(
235    rename_all = "camelCase",
236    rename_all_fields = "camelCase",
237    tag = "state",
238    content = "data"
239)]
240enum UserProfileCacheEntry {
241    /// A request has been issued and we're waiting for it to complete.
242    Requested,
243    /// The profile has been successfully loaded from the server.
244    Loaded {
245        #[serde(flatten)]
246        user_profile: UserProfile,
247        rooms: BTreeSet<OwnedRoomId>,
248    },
249}