use crossbeam_queue::SegQueue;
use matrix_sdk::{
room::RoomMember,
ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId, UserId},
};
use serde::Serialize;
use std::{
cell::RefCell,
collections::{BTreeMap, btree_map::Entry},
};
use tokio::sync::oneshot;
use tracing::warn;
use crate::{MatrixRequest, commands::submit_async_request};
thread_local! {
static USER_PROFILE_CACHE: RefCell<BTreeMap<OwnedUserId, UserProfileCacheEntry>> = const { RefCell::new(BTreeMap::new()) };
}
#[derive(Debug, Clone)]
pub(crate) enum UserProfileCacheEntry {
Requested,
Loaded {
user_profile: UserProfile,
rooms: BTreeMap<OwnedRoomId, RoomMember>,
},
}
static PENDING_USER_PROFILE_UPDATES: SegQueue<UserProfileUpdate> = SegQueue::new();
pub fn enqueue_user_profile_update(update: UserProfileUpdate) {
PENDING_USER_PROFILE_UPDATES.push(update);
}
pub enum UserProfileUpdate {
Full {
new_profile: UserProfile,
room_id: OwnedRoomId,
room_member: RoomMember,
},
RoomMemberOnly {
room_id: OwnedRoomId,
room_member: RoomMember,
},
UserProfileOnly(UserProfile),
}
impl UserProfileUpdate {
#[allow(unused)]
pub fn user_id(&self) -> &UserId {
match self {
UserProfileUpdate::Full { new_profile, .. } => &new_profile.user_id,
UserProfileUpdate::RoomMemberOnly { room_member, .. } => room_member.user_id(),
UserProfileUpdate::UserProfileOnly(profile) => &profile.user_id,
}
}
pub fn get_user_profile_from_update(&self) -> Option<&UserProfile> {
match self {
UserProfileUpdate::Full { new_profile, .. } => Some(new_profile),
UserProfileUpdate::RoomMemberOnly { .. } => None,
UserProfileUpdate::UserProfileOnly(profile) => Some(profile),
}
}
fn apply_to_cache(self, cache: &mut BTreeMap<OwnedUserId, UserProfileCacheEntry>) {
match self {
UserProfileUpdate::Full {
new_profile,
room_id,
room_member,
} => match cache.entry(new_profile.user_id.clone()) {
Entry::Occupied(mut entry) => match entry.get_mut() {
e @ UserProfileCacheEntry::Requested => {
*e = UserProfileCacheEntry::Loaded {
user_profile: new_profile,
rooms: {
let mut room_members_map = BTreeMap::new();
room_members_map.insert(room_id, room_member);
room_members_map
},
};
}
UserProfileCacheEntry::Loaded {
user_profile,
rooms,
} => {
*user_profile = new_profile;
rooms.insert(room_id, room_member);
}
},
Entry::Vacant(entry) => {
entry.insert(UserProfileCacheEntry::Loaded {
user_profile: new_profile,
rooms: {
let mut room_members_map = BTreeMap::new();
room_members_map.insert(room_id, room_member);
room_members_map
},
});
}
},
UserProfileUpdate::RoomMemberOnly {
room_id,
room_member,
} => {
match cache.entry(room_member.user_id().to_owned()) {
Entry::Occupied(mut entry) => match entry.get_mut() {
e @ UserProfileCacheEntry::Requested => {
warn!(
"BUG: User profile cache entry was `Requested` for user {} when handling RoomMemberOnly update",
room_member.user_id()
);
*e = UserProfileCacheEntry::Loaded {
user_profile: UserProfile {
user_id: room_member.user_id().to_owned(),
username: None,
avatar: room_member.avatar_url().map(|url| url.to_owned()),
},
rooms: {
let mut room_members_map = BTreeMap::new();
room_members_map.insert(room_id, room_member);
room_members_map
},
};
}
UserProfileCacheEntry::Loaded { rooms, .. } => {
rooms.insert(room_id, room_member);
}
},
Entry::Vacant(entry) => {
warn!(
"BUG: User profile cache entry not found for user {} when handling RoomMemberOnly update",
room_member.user_id()
);
entry.insert(UserProfileCacheEntry::Loaded {
user_profile: UserProfile {
user_id: room_member.user_id().to_owned(),
username: None,
avatar: room_member.avatar_url().map(|url| url.to_owned()),
},
rooms: {
let mut room_members_map = BTreeMap::new();
room_members_map.insert(room_id, room_member);
room_members_map
},
});
}
}
}
UserProfileUpdate::UserProfileOnly(new_profile) => {
match cache.entry(new_profile.user_id.clone()) {
Entry::Occupied(mut entry) => match entry.get_mut() {
e @ UserProfileCacheEntry::Requested => {
*e = UserProfileCacheEntry::Loaded {
user_profile: new_profile,
rooms: BTreeMap::new(),
};
}
UserProfileCacheEntry::Loaded { user_profile, .. } => {
*user_profile = new_profile;
}
},
Entry::Vacant(entry) => {
entry.insert(UserProfileCacheEntry::Loaded {
user_profile: new_profile,
rooms: BTreeMap::new(),
});
}
}
}
}
}
}
pub fn process_user_profile_updates() {
USER_PROFILE_CACHE.with_borrow_mut(|cache| {
while let Some(update) = PENDING_USER_PROFILE_UPDATES.pop() {
update.apply_to_cache(cache);
}
});
}
pub fn with_sender(
user_id: OwnedUserId,
room_id: Option<&OwnedRoomId>,
fetch_if_missing: bool,
sender: oneshot::Sender<Option<UserProfile>>,
) {
USER_PROFILE_CACHE.with_borrow_mut(|cache| match cache.entry(user_id) {
Entry::Occupied(entry) => match entry.get() {
UserProfileCacheEntry::Loaded {
user_profile,
rooms,
} => {
if room_id.is_some_and(|id| !rooms.contains_key(id)) {
submit_async_request(MatrixRequest::GetUserProfile {
user_id: entry.key().clone(),
room_id: room_id.cloned(),
local_only: false,
sender: None,
});
}
let _ = sender.send(Some(user_profile.to_owned()));
}
UserProfileCacheEntry::Requested => {
}
},
Entry::Vacant(entry) => {
if fetch_if_missing {
submit_async_request(MatrixRequest::GetUserProfile {
user_id: entry.key().clone(),
room_id: room_id.cloned(),
local_only: false,
sender: Some(sender),
});
entry.insert(UserProfileCacheEntry::Requested);
}
}
})
}
pub enum CachedName {
FoundInRoom(Option<String>),
FoundInProfile(Option<String>),
NotFound,
}
impl CachedName {
pub fn was_found(&self) -> bool {
matches!(self, Self::FoundInRoom(_) | Self::FoundInProfile(_))
}
pub fn into_option(self) -> Option<String> {
self.into()
}
pub fn as_deref(&self) -> Option<&str> {
match self {
CachedName::FoundInRoom(name) | CachedName::FoundInProfile(name) => name.as_deref(),
CachedName::NotFound => None,
}
}
}
impl From<CachedName> for Option<String> {
fn from(cached_name: CachedName) -> Self {
match cached_name {
CachedName::FoundInRoom(name) => name,
CachedName::FoundInProfile(name) => name,
CachedName::NotFound => None,
}
}
}
pub fn _clear_user_profile_cache() {
USER_PROFILE_CACHE.with_borrow_mut(|cache| {
cache.clear();
});
}
#[derive(Clone, Debug, Serialize)]
pub struct UserProfile {
pub user_id: OwnedUserId,
pub username: Option<String>,
pub avatar: Option<OwnedMxcUri>,
}
impl UserProfile {
pub fn displayable_name(&self) -> &str {
if let Some(un) = self.username.as_ref()
&& !un.is_empty()
{
return un.as_str();
}
self.user_id.as_str()
}
}