17 changed files with 906 additions and 412 deletions
@ -0,0 +1,108 @@
|
||||
use std::{cell::RefCell, fmt, rc::Rc}; |
||||
|
||||
use ruma::{OwnedRoomOrAliasId, OwnedUserId, RoomId}; |
||||
use wtinylfu::WTinyLfuCache; |
||||
|
||||
use super::{RemoteRoom, RemoteUser}; |
||||
use crate::{session::model::Session, utils::matrix::MatrixRoomIdUri}; |
||||
|
||||
/// The data of the [`RemoteCache`].
|
||||
struct RemoteCacheData { |
||||
/// Remote rooms.
|
||||
rooms: RefCell<WTinyLfuCache<OwnedRoomOrAliasId, RemoteRoom>>, |
||||
/// Remote users.
|
||||
users: RefCell<WTinyLfuCache<OwnedUserId, RemoteUser>>, |
||||
} |
||||
|
||||
/// An API to query remote data and cache it.
|
||||
#[derive(Clone)] |
||||
pub(crate) struct RemoteCache { |
||||
session: Session, |
||||
data: Rc<RemoteCacheData>, |
||||
} |
||||
|
||||
impl RemoteCache { |
||||
/// Construct a new `RemoteCache` for the given session.
|
||||
pub(crate) fn new(session: Session) -> Self { |
||||
Self { |
||||
session, |
||||
data: RemoteCacheData { |
||||
rooms: WTinyLfuCache::new(30, 10).into(), |
||||
users: WTinyLfuCache::new(30, 10).into(), |
||||
} |
||||
.into(), |
||||
} |
||||
} |
||||
|
||||
/// Get the remote room for the given URI.
|
||||
pub(crate) fn room(&self, uri: MatrixRoomIdUri) -> RemoteRoom { |
||||
let mut rooms = self.data.rooms.borrow_mut(); |
||||
|
||||
// Check if the room is in the cache.
|
||||
if let Some(room) = rooms.get(&uri.id) { |
||||
room.load_data_if_stale(); |
||||
return room.clone(); |
||||
} |
||||
|
||||
// Check if the alias or ID matches a room in the cache, in case the URI uses
|
||||
// another ID than the one we used as a key for the cache.
|
||||
let mut found_id = None; |
||||
let id_or_alias = <&RoomId>::try_from(&*uri.id); |
||||
|
||||
for (id, room) in rooms.iter() { |
||||
match id_or_alias { |
||||
Ok(room_id) => { |
||||
if room.room_id().is_some_and(|id| id == room_id) { |
||||
found_id = Some(id.clone()); |
||||
break; |
||||
} |
||||
} |
||||
Err(room_alias) => { |
||||
if room |
||||
.canonical_alias() |
||||
.is_some_and(|alias| alias == room_alias) |
||||
{ |
||||
found_id = Some(id.clone()); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
if let Some(id) = found_id { |
||||
let room = rooms.get(&id).expect("room should be in cache"); |
||||
room.load_data_if_stale(); |
||||
return room.clone(); |
||||
} |
||||
|
||||
// We did not find it, create the room.
|
||||
let id = uri.id.clone(); |
||||
let room = RemoteRoom::new(&self.session, uri); |
||||
rooms.push(id, room.clone()); |
||||
|
||||
room |
||||
} |
||||
|
||||
/// Get the remote user for the given ID.
|
||||
pub(crate) fn user(&self, user_id: OwnedUserId) -> RemoteUser { |
||||
let mut users = self.data.users.borrow_mut(); |
||||
|
||||
// Check if the user is in the cache.
|
||||
if let Some(user) = users.get(&user_id) { |
||||
user.load_profile_if_stale(); |
||||
return user.clone(); |
||||
} |
||||
|
||||
// We did not find it, create the user.
|
||||
let user = RemoteUser::new(&self.session, user_id.clone()); |
||||
users.push(user_id, user.clone()); |
||||
|
||||
user |
||||
} |
||||
} |
||||
|
||||
impl fmt::Debug for RemoteCache { |
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
f.debug_struct("RemoteCache").finish_non_exhaustive() |
||||
} |
||||
} |
||||
@ -0,0 +1,5 @@
|
||||
mod cache; |
||||
mod room; |
||||
mod user; |
||||
|
||||
pub(crate) use self::{cache::*, room::*, user::*}; |
||||
@ -0,0 +1,472 @@
|
||||
use std::{cell::RefCell, time::Duration}; |
||||
|
||||
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; |
||||
use matrix_sdk::reqwest::StatusCode; |
||||
use ruma::{ |
||||
api::client::{ |
||||
room::get_summary, |
||||
space::{get_hierarchy, SpaceHierarchyRoomsChunk}, |
||||
}, |
||||
assign, uint, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, |
||||
}; |
||||
use tracing::{debug, warn}; |
||||
|
||||
use crate::{ |
||||
components::{AvatarImage, AvatarUriSource, PillSource}, |
||||
prelude::*, |
||||
session::model::Session, |
||||
spawn, spawn_tokio, |
||||
utils::{matrix::MatrixRoomIdUri, string::linkify, AbortableHandle, LoadingState}, |
||||
}; |
||||
|
||||
/// The time after which the data of a room is assumed to be stale.
|
||||
///
|
||||
/// This matches 1 day.
|
||||
const DATA_VALIDITY_DURATION: Duration = Duration::from_secs(24 * 60 * 60); |
||||
|
||||
mod imp { |
||||
use std::{ |
||||
cell::{Cell, OnceCell}, |
||||
time::Instant, |
||||
}; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Default, glib::Properties)] |
||||
#[properties(wrapper_type = super::RemoteRoom)] |
||||
pub struct RemoteRoom { |
||||
/// The current session.
|
||||
#[property(get, set = Self::set_session, construct_only)] |
||||
session: glib::WeakRef<Session>, |
||||
/// The Matrix URI of this room.
|
||||
uri: OnceCell<MatrixRoomIdUri>, |
||||
/// The ID of this room.
|
||||
room_id: RefCell<Option<OwnedRoomId>>, |
||||
/// The canonical alias of this room.
|
||||
canonical_alias: RefCell<Option<OwnedRoomAliasId>>, |
||||
/// The name that is set for this room.
|
||||
///
|
||||
/// This can be empty, the display name should be used instead in the
|
||||
/// interface.
|
||||
#[property(get)] |
||||
name: RefCell<Option<String>>, |
||||
/// The topic of this room.
|
||||
#[property(get)] |
||||
topic: RefCell<Option<String>>, |
||||
/// The linkified topic of this room.
|
||||
///
|
||||
/// This is the string that should be used in the interface when markup
|
||||
/// is allowed.
|
||||
#[property(get)] |
||||
topic_linkified: RefCell<Option<String>>, |
||||
/// The number of joined members in the room.
|
||||
#[property(get)] |
||||
joined_members_count: Cell<u32>, |
||||
/// The loading state.
|
||||
#[property(get, builder(LoadingState::default()))] |
||||
loading_state: Cell<LoadingState>, |
||||
// The time of the last request.
|
||||
last_request_time: Cell<Option<Instant>>, |
||||
request_abort_handle: AbortableHandle, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for RemoteRoom { |
||||
const NAME: &'static str = "RemoteRoom"; |
||||
type Type = super::RemoteRoom; |
||||
type ParentType = PillSource; |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for RemoteRoom {} |
||||
|
||||
impl PillSourceImpl for RemoteRoom { |
||||
fn identifier(&self) -> String { |
||||
self.uri.get().unwrap().id.to_string() |
||||
} |
||||
} |
||||
|
||||
impl RemoteRoom { |
||||
/// Set the current session.
|
||||
fn set_session(&self, session: &Session) { |
||||
self.session.set(Some(session)); |
||||
|
||||
self.obj().avatar_data().set_image(Some(AvatarImage::new( |
||||
session, |
||||
AvatarUriSource::Room, |
||||
None, |
||||
None, |
||||
))); |
||||
} |
||||
|
||||
/// Set the Matrix URI of this room.
|
||||
pub(super) fn set_uri(&self, uri: MatrixRoomIdUri) { |
||||
if let Ok(room_id) = uri.id.clone().try_into() { |
||||
self.set_room_id(room_id); |
||||
} |
||||
|
||||
self.uri |
||||
.set(uri) |
||||
.expect("Matrix URI should be uninitialized"); |
||||
|
||||
self.update_display_name(); |
||||
} |
||||
|
||||
/// The Matrix URI of this room.
|
||||
pub(super) fn uri(&self) -> &MatrixRoomIdUri { |
||||
self.uri.get().expect("Matrix URI should be initialized") |
||||
} |
||||
|
||||
/// Set the ID of this room.
|
||||
fn set_room_id(&self, room_id: OwnedRoomId) { |
||||
self.room_id.replace(Some(room_id)); |
||||
} |
||||
|
||||
/// The ID of this room.
|
||||
pub(super) fn room_id(&self) -> Option<OwnedRoomId> { |
||||
self.room_id.borrow().clone() |
||||
} |
||||
|
||||
/// Set the canonical alias of this room.
|
||||
fn set_canonical_alias(&self, alias: Option<OwnedRoomAliasId>) { |
||||
if *self.canonical_alias.borrow() == alias { |
||||
return; |
||||
} |
||||
|
||||
self.canonical_alias.replace(alias); |
||||
self.update_display_name(); |
||||
} |
||||
|
||||
/// The canonical alias of this room.
|
||||
pub(super) fn canonical_alias(&self) -> Option<OwnedRoomAliasId> { |
||||
self.canonical_alias |
||||
.borrow() |
||||
.clone() |
||||
.or_else(|| self.uri().id.clone().try_into().ok()) |
||||
} |
||||
|
||||
/// Set the name of this room.
|
||||
fn set_name(&self, name: Option<String>) { |
||||
if *self.name.borrow() == name { |
||||
return; |
||||
} |
||||
|
||||
self.name.replace(name); |
||||
|
||||
self.obj().notify_name(); |
||||
self.update_display_name(); |
||||
} |
||||
|
||||
/// The display name of this room.
|
||||
pub(super) fn update_display_name(&self) { |
||||
let display_name = self |
||||
.name |
||||
.borrow() |
||||
.clone() |
||||
.or_else(|| { |
||||
self.canonical_alias |
||||
.borrow() |
||||
.as_ref() |
||||
.map(ToString::to_string) |
||||
}) |
||||
.unwrap_or_else(|| self.identifier()); |
||||
|
||||
self.obj().set_display_name(display_name); |
||||
} |
||||
|
||||
/// Set the topic of this room.
|
||||
fn set_topic(&self, topic: Option<String>) { |
||||
let topic = |
||||
topic.filter(|s| !s.is_empty() && s.find(|c: char| !c.is_whitespace()).is_some()); |
||||
|
||||
if *self.topic.borrow() == topic { |
||||
return; |
||||
} |
||||
|
||||
let topic_linkified = topic.as_deref().map(|t| { |
||||
// Detect links.
|
||||
let mut s = linkify(t); |
||||
// Remove trailing spaces.
|
||||
s.truncate_end_whitespaces(); |
||||
s |
||||
}); |
||||
|
||||
self.topic.replace(topic); |
||||
self.topic_linkified.replace(topic_linkified); |
||||
|
||||
let obj = self.obj(); |
||||
obj.notify_topic(); |
||||
obj.notify_topic_linkified(); |
||||
} |
||||
|
||||
/// Set the loading state.
|
||||
fn set_joined_members_count(&self, count: u32) { |
||||
if self.joined_members_count.get() == count { |
||||
return; |
||||
} |
||||
|
||||
self.joined_members_count.set(count); |
||||
self.obj().notify_joined_members_count(); |
||||
} |
||||
|
||||
/// Set the loading state.
|
||||
pub(super) fn set_loading_state(&self, loading_state: LoadingState) { |
||||
if self.loading_state.get() == loading_state { |
||||
return; |
||||
} |
||||
|
||||
self.loading_state.set(loading_state); |
||||
|
||||
if loading_state == LoadingState::Error { |
||||
// Reset the request time so we try it again the next time.
|
||||
self.last_request_time.take(); |
||||
} |
||||
|
||||
self.obj().notify_loading_state(); |
||||
} |
||||
|
||||
/// Set the room data.
|
||||
pub(super) fn set_data(&self, data: RemoteRoomData) { |
||||
self.set_room_id(data.room_id); |
||||
self.set_canonical_alias(data.canonical_alias); |
||||
self.set_name(data.name); |
||||
self.set_topic(data.topic); |
||||
self.set_joined_members_count(data.joined_members_count); |
||||
|
||||
if let Some(image) = self.obj().avatar_data().image() { |
||||
image.set_uri_and_info(data.avatar_url, None); |
||||
} |
||||
|
||||
self.set_loading_state(LoadingState::Ready); |
||||
} |
||||
|
||||
/// Whether the data of the room is considered to be stale.
|
||||
pub(super) fn is_data_stale(&self) -> bool { |
||||
self.last_request_time |
||||
.get() |
||||
.is_none_or(|last_time| last_time.elapsed() > DATA_VALIDITY_DURATION) |
||||
} |
||||
|
||||
/// Update the last request time to now.
|
||||
pub(super) fn update_last_request_time(&self) { |
||||
self.last_request_time.set(Some(Instant::now())); |
||||
} |
||||
|
||||
/// Request the data of this room.
|
||||
pub(super) async fn load_data(&self) { |
||||
let Some(session) = self.session.upgrade() else { |
||||
self.last_request_time.take(); |
||||
return; |
||||
}; |
||||
|
||||
self.set_loading_state(LoadingState::Loading); |
||||
|
||||
// Try to load data from the summary endpoint first, and if it is not supported
|
||||
// try the space hierarchy endpoint.
|
||||
if !self.load_data_from_summary(&session).await { |
||||
self.load_data_from_space_hierarchy(&session).await; |
||||
} |
||||
} |
||||
|
||||
/// Load the data of this room using the room summary endpoint.
|
||||
///
|
||||
/// At the time of writing this code, MSC3266 has been accepted but the
|
||||
/// endpoint is not part of a Matrix spec release.
|
||||
///
|
||||
/// Returns `false` if the endpoint is not supported by the homeserver.
|
||||
async fn load_data_from_summary(&self, session: &Session) -> bool { |
||||
let uri = self.uri(); |
||||
let client = session.client(); |
||||
|
||||
let request = get_summary::msc3266::Request::new(uri.id.clone(), uri.via.clone()); |
||||
let handle = spawn_tokio!(async move { client.send(request).await }); |
||||
|
||||
let Some(result) = self.request_abort_handle.await_task(handle).await else { |
||||
// The task was aborted, which means that the object was dropped.
|
||||
return true; |
||||
}; |
||||
|
||||
match result { |
||||
Ok(response) => { |
||||
self.set_data(response.into()); |
||||
true |
||||
} |
||||
Err(error) => { |
||||
if error |
||||
.as_client_api_error() |
||||
.is_some_and(|error| error.status_code == StatusCode::NOT_FOUND) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
warn!( |
||||
"Could not get room details from summary endpoint for room `{}`: {error}", |
||||
uri.id |
||||
); |
||||
self.set_loading_state(LoadingState::Error); |
||||
true |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Load the data of this room using the space hierarchy endpoint.
|
||||
///
|
||||
/// This endpoint should work for any room already known by the
|
||||
/// homeserver.
|
||||
async fn load_data_from_space_hierarchy(&self, session: &Session) { |
||||
let uri = self.uri(); |
||||
let client = session.client(); |
||||
|
||||
// The endpoint only works with a room ID.
|
||||
let room_id = match OwnedRoomId::try_from(uri.id.clone()) { |
||||
Ok(room_id) => room_id, |
||||
Err(alias) => { |
||||
let client_clone = client.clone(); |
||||
let handle = |
||||
spawn_tokio!(async move { client_clone.resolve_room_alias(&alias).await }); |
||||
|
||||
let Some(result) = self.request_abort_handle.await_task(handle).await else { |
||||
// The task was aborted, which means that the object was dropped.
|
||||
return; |
||||
}; |
||||
|
||||
match result { |
||||
Ok(response) => response.room_id, |
||||
Err(error) => { |
||||
warn!("Could not resolve room alias `{}`: {error}", uri.id); |
||||
self.set_loading_state(LoadingState::Error); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
|
||||
let request = assign!(get_hierarchy::v1::Request::new(room_id.clone()), { |
||||
// We are only interested in the single room.
|
||||
limit: Some(uint!(1)) |
||||
}); |
||||
let handle = spawn_tokio!(async move { client.send(request).await }); |
||||
|
||||
let Some(result) = self.request_abort_handle.await_task(handle).await else { |
||||
// The task was aborted, which means that the object was dropped.
|
||||
return; |
||||
}; |
||||
|
||||
match result { |
||||
Ok(response) => { |
||||
if let Some(chunk) = response |
||||
.rooms |
||||
.into_iter() |
||||
.next() |
||||
.filter(|c| c.room_id == room_id) |
||||
{ |
||||
self.set_data(chunk.into()); |
||||
} else { |
||||
debug!("Space hierarchy endpoint did not return requested room"); |
||||
self.set_loading_state(LoadingState::Error); |
||||
} |
||||
} |
||||
Err(error) => { |
||||
warn!( |
||||
"Could not get room details from space hierarchy endpoint for room `{}`: {error}", |
||||
uri.id |
||||
); |
||||
self.set_loading_state(LoadingState::Error); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A Room that can only be updated by making remote calls, i.e. it won't be updated via sync.
|
||||
pub struct RemoteRoom(ObjectSubclass<imp::RemoteRoom>) |
||||
@extends PillSource; |
||||
} |
||||
|
||||
impl RemoteRoom { |
||||
pub(super) fn new(session: &Session, uri: MatrixRoomIdUri) -> Self { |
||||
let obj = glib::Object::builder::<Self>() |
||||
.property("session", session) |
||||
.build(); |
||||
|
||||
obj.imp().set_uri(uri); |
||||
obj.load_data_if_stale(); |
||||
|
||||
obj |
||||
} |
||||
|
||||
/// The Matrix URI of this room.
|
||||
pub(crate) fn uri(&self) -> &MatrixRoomIdUri { |
||||
self.imp().uri() |
||||
} |
||||
|
||||
/// The ID of this room.
|
||||
pub(crate) fn room_id(&self) -> Option<OwnedRoomId> { |
||||
self.imp().room_id() |
||||
} |
||||
|
||||
/// The canonical alias of this room.
|
||||
pub(crate) fn canonical_alias(&self) -> Option<OwnedRoomAliasId> { |
||||
self.imp().canonical_alias() |
||||
} |
||||
|
||||
/// Load the data of this room if it is considered to be stale.
|
||||
pub(super) fn load_data_if_stale(&self) { |
||||
let imp = self.imp(); |
||||
|
||||
if !imp.is_data_stale() { |
||||
// The data is still valid, nothing to do.
|
||||
return; |
||||
} |
||||
|
||||
// Set the request time right away, to prevent several requests at the same
|
||||
// time.
|
||||
imp.update_last_request_time(); |
||||
|
||||
spawn!(clone!( |
||||
#[weak] |
||||
imp, |
||||
async move { |
||||
imp.load_data().await; |
||||
} |
||||
)); |
||||
} |
||||
} |
||||
|
||||
/// The remote room data.
|
||||
#[derive(Debug)] |
||||
struct RemoteRoomData { |
||||
room_id: OwnedRoomId, |
||||
canonical_alias: Option<OwnedRoomAliasId>, |
||||
name: Option<String>, |
||||
topic: Option<String>, |
||||
avatar_url: Option<OwnedMxcUri>, |
||||
joined_members_count: u32, |
||||
} |
||||
|
||||
impl From<get_summary::msc3266::Response> for RemoteRoomData { |
||||
fn from(value: get_summary::msc3266::Response) -> Self { |
||||
Self { |
||||
room_id: value.room_id, |
||||
canonical_alias: value.canonical_alias, |
||||
name: value.name, |
||||
topic: value.topic, |
||||
avatar_url: value.avatar_url, |
||||
joined_members_count: value.num_joined_members.try_into().unwrap_or(u32::MAX), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<SpaceHierarchyRoomsChunk> for RemoteRoomData { |
||||
fn from(value: SpaceHierarchyRoomsChunk) -> Self { |
||||
Self { |
||||
room_id: value.room_id, |
||||
canonical_alias: value.canonical_alias, |
||||
name: value.name, |
||||
topic: value.topic, |
||||
avatar_url: value.avatar_url, |
||||
joined_members_count: value.num_joined_members.try_into().unwrap_or(u32::MAX), |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,155 @@
|
||||
use std::time::{Duration, Instant}; |
||||
|
||||
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; |
||||
use matrix_sdk::ruma::OwnedUserId; |
||||
use tracing::error; |
||||
|
||||
use crate::{ |
||||
components::PillSource, |
||||
prelude::*, |
||||
session::model::{Session, User}, |
||||
spawn, spawn_tokio, |
||||
utils::{AbortableHandle, LoadingState}, |
||||
}; |
||||
|
||||
/// The time after which the profile of a user is assumed to be stale.
|
||||
///
|
||||
/// This matches 1 hour.
|
||||
const PROFILE_VALIDITY_DURATION: Duration = Duration::from_secs(60 * 60); |
||||
|
||||
mod imp { |
||||
use std::cell::Cell; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, glib::Properties)] |
||||
#[properties(wrapper_type = super::RemoteUser)] |
||||
pub struct RemoteUser { |
||||
// The loading state of the profile.
|
||||
#[property(get, builder(LoadingState::default()))] |
||||
loading_state: Cell<LoadingState>, |
||||
// The time of the last request.
|
||||
last_request_time: Cell<Option<Instant>>, |
||||
request_abort_handle: AbortableHandle, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for RemoteUser { |
||||
const NAME: &'static str = "RemoteUser"; |
||||
type Type = super::RemoteUser; |
||||
type ParentType = User; |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for RemoteUser {} |
||||
|
||||
impl PillSourceImpl for RemoteUser { |
||||
fn identifier(&self) -> String { |
||||
self.obj().upcast_ref::<User>().user_id_string() |
||||
} |
||||
} |
||||
|
||||
impl RemoteUser { |
||||
/// Set the loading state.
|
||||
pub(super) fn set_loading_state(&self, loading_state: LoadingState) { |
||||
if self.loading_state.get() == loading_state { |
||||
return; |
||||
} |
||||
|
||||
self.loading_state.set(loading_state); |
||||
|
||||
if loading_state == LoadingState::Error { |
||||
// Reset the request time so we try it again the next time.
|
||||
self.last_request_time.take(); |
||||
} |
||||
|
||||
self.obj().notify_loading_state(); |
||||
} |
||||
|
||||
/// Whether the profile of the user is considered to be stale.
|
||||
pub(super) fn is_profile_stale(&self) -> bool { |
||||
self.last_request_time |
||||
.get() |
||||
.is_none_or(|last_time| last_time.elapsed() > PROFILE_VALIDITY_DURATION) |
||||
} |
||||
|
||||
/// Update the last request time to now.
|
||||
pub(super) fn update_last_request_time(&self) { |
||||
self.last_request_time.set(Some(Instant::now())); |
||||
} |
||||
|
||||
/// Request the profile of this user.
|
||||
pub(super) async fn load_profile(&self) { |
||||
let obj = self.obj(); |
||||
|
||||
self.set_loading_state(LoadingState::Loading); |
||||
|
||||
let user_id = obj.user_id(); |
||||
|
||||
let client = obj.session().client(); |
||||
let user_id_clone = user_id.clone(); |
||||
let handle = spawn_tokio!(async move { |
||||
client.account().fetch_user_profile_of(&user_id_clone).await |
||||
}); |
||||
|
||||
let Some(result) = self.request_abort_handle.await_task(handle).await else { |
||||
// The task was aborted, which means that the object was dropped.
|
||||
return; |
||||
}; |
||||
|
||||
let profile = match result { |
||||
Ok(profile) => profile, |
||||
Err(error) => { |
||||
error!("Could not load profile for user `{user_id}`: {error}"); |
||||
self.set_loading_state(LoadingState::Error); |
||||
return; |
||||
} |
||||
}; |
||||
|
||||
obj.set_name(profile.displayname); |
||||
obj.set_avatar_url(profile.avatar_url); |
||||
self.set_loading_state(LoadingState::Ready); |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A User that can only be updated by making remote calls, i.e. it won't be updated via sync.
|
||||
pub struct RemoteUser(ObjectSubclass<imp::RemoteUser>) @extends PillSource, User; |
||||
} |
||||
|
||||
impl RemoteUser { |
||||
pub(super) fn new(session: &Session, user_id: OwnedUserId) -> Self { |
||||
let obj = glib::Object::builder::<Self>() |
||||
.property("session", session) |
||||
.build(); |
||||
|
||||
obj.upcast_ref::<User>().imp().set_user_id(user_id); |
||||
obj.load_profile_if_stale(); |
||||
|
||||
obj |
||||
} |
||||
|
||||
/// Request this user's profile from the homeserver if it is considered to
|
||||
/// be stale.
|
||||
pub(super) fn load_profile_if_stale(&self) { |
||||
let imp = self.imp(); |
||||
|
||||
if !imp.is_profile_stale() { |
||||
// The data is still valid, nothing to do.
|
||||
return; |
||||
} |
||||
|
||||
// Set the request time right away, to prevent several requests at the same
|
||||
// time.
|
||||
imp.update_last_request_time(); |
||||
|
||||
spawn!(clone!( |
||||
#[weak] |
||||
imp, |
||||
async move { |
||||
imp.load_profile().await; |
||||
} |
||||
)); |
||||
} |
||||
} |
||||
@ -1,293 +0,0 @@
|
||||
use std::cell::RefCell; |
||||
|
||||
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; |
||||
use ruma::{ |
||||
api::client::space::{get_hierarchy, SpaceHierarchyRoomsChunk}, |
||||
assign, uint, OwnedRoomAliasId, OwnedRoomId, |
||||
}; |
||||
use tracing::{debug, warn}; |
||||
|
||||
use super::Session; |
||||
use crate::{ |
||||
components::{AvatarImage, AvatarUriSource, PillSource}, |
||||
prelude::*, |
||||
spawn, spawn_tokio, |
||||
utils::{matrix::MatrixRoomIdUri, string::linkify, LoadingState}, |
||||
}; |
||||
|
||||
mod imp { |
||||
use std::cell::{Cell, OnceCell}; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Default, glib::Properties)] |
||||
#[properties(wrapper_type = super::RemoteRoom)] |
||||
pub struct RemoteRoom { |
||||
/// The current session.
|
||||
#[property(get, set = Self::set_session, construct_only)] |
||||
session: glib::WeakRef<Session>, |
||||
/// The Matrix URI of this room.
|
||||
uri: OnceCell<MatrixRoomIdUri>, |
||||
/// The canonical alias of this room.
|
||||
alias: RefCell<Option<OwnedRoomAliasId>>, |
||||
/// The name that is set for this room.
|
||||
///
|
||||
/// This can be empty, the display name should be used instead in the
|
||||
/// interface.
|
||||
#[property(get)] |
||||
name: RefCell<Option<String>>, |
||||
/// The topic of this room.
|
||||
#[property(get)] |
||||
topic: RefCell<Option<String>>, |
||||
/// The linkified topic of this room.
|
||||
///
|
||||
/// This is the string that should be used in the interface when markup
|
||||
/// is allowed.
|
||||
#[property(get)] |
||||
topic_linkified: RefCell<Option<String>>, |
||||
/// The number of joined members in the room.
|
||||
#[property(get)] |
||||
joined_members_count: Cell<u32>, |
||||
/// The loading state.
|
||||
#[property(get, builder(LoadingState::default()))] |
||||
loading_state: Cell<LoadingState>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for RemoteRoom { |
||||
const NAME: &'static str = "RemoteRoom"; |
||||
type Type = super::RemoteRoom; |
||||
type ParentType = PillSource; |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for RemoteRoom {} |
||||
|
||||
impl PillSourceImpl for RemoteRoom { |
||||
fn identifier(&self) -> String { |
||||
self.uri.get().unwrap().id.to_string() |
||||
} |
||||
} |
||||
|
||||
impl RemoteRoom { |
||||
/// Set the current session.
|
||||
fn set_session(&self, session: &Session) { |
||||
self.session.set(Some(session)); |
||||
|
||||
self.obj().avatar_data().set_image(Some(AvatarImage::new( |
||||
session, |
||||
AvatarUriSource::Room, |
||||
None, |
||||
None, |
||||
))); |
||||
} |
||||
|
||||
/// Set the Matrix URI of this room.
|
||||
pub(super) fn set_uri(&self, uri: MatrixRoomIdUri) { |
||||
self.uri |
||||
.set(uri) |
||||
.expect("Matrix URI should be uninitialized"); |
||||
|
||||
self.update_display_name(); |
||||
|
||||
spawn!(clone!( |
||||
#[weak(rename_to = imp)] |
||||
self, |
||||
async move { |
||||
imp.load().await; |
||||
} |
||||
)); |
||||
} |
||||
|
||||
/// The Matrix URI of this room.
|
||||
pub(super) fn uri(&self) -> &MatrixRoomIdUri { |
||||
self.uri.get().expect("Matrix URI should be initialized") |
||||
} |
||||
|
||||
/// Set the alias of this room.
|
||||
fn set_alias(&self, alias: Option<OwnedRoomAliasId>) { |
||||
if *self.alias.borrow() == alias { |
||||
return; |
||||
} |
||||
|
||||
self.alias.replace(alias); |
||||
self.update_display_name(); |
||||
} |
||||
|
||||
/// The canonical alias of this room.
|
||||
pub(super) fn alias(&self) -> Option<OwnedRoomAliasId> { |
||||
self.alias |
||||
.borrow() |
||||
.clone() |
||||
.or_else(|| self.uri().id.clone().try_into().ok()) |
||||
} |
||||
|
||||
/// Set the name of this room.
|
||||
fn set_name(&self, name: Option<String>) { |
||||
if *self.name.borrow() == name { |
||||
return; |
||||
} |
||||
|
||||
self.name.replace(name); |
||||
|
||||
self.obj().notify_name(); |
||||
self.update_display_name(); |
||||
} |
||||
|
||||
/// The display name of this room.
|
||||
pub(super) fn update_display_name(&self) { |
||||
let display_name = self |
||||
.name |
||||
.borrow() |
||||
.clone() |
||||
.or_else(|| self.alias.borrow().as_ref().map(ToString::to_string)) |
||||
.unwrap_or_else(|| self.identifier()); |
||||
|
||||
self.obj().set_display_name(display_name); |
||||
} |
||||
|
||||
/// Set the topic of this room.
|
||||
fn set_topic(&self, topic: Option<String>) { |
||||
let topic = |
||||
topic.filter(|s| !s.is_empty() && s.find(|c: char| !c.is_whitespace()).is_some()); |
||||
|
||||
if *self.topic.borrow() == topic { |
||||
return; |
||||
} |
||||
|
||||
let topic_linkified = topic.as_deref().map(|t| { |
||||
// Detect links.
|
||||
let mut s = linkify(t); |
||||
// Remove trailing spaces.
|
||||
s.truncate_end_whitespaces(); |
||||
s |
||||
}); |
||||
|
||||
self.topic.replace(topic); |
||||
self.topic_linkified.replace(topic_linkified); |
||||
|
||||
let obj = self.obj(); |
||||
obj.notify_topic(); |
||||
obj.notify_topic_linkified(); |
||||
} |
||||
|
||||
/// Set the loading state.
|
||||
fn set_joined_members_count(&self, count: u32) { |
||||
if self.joined_members_count.get() == count { |
||||
return; |
||||
} |
||||
|
||||
self.joined_members_count.set(count); |
||||
self.obj().notify_joined_members_count(); |
||||
} |
||||
|
||||
/// Set the loading state.
|
||||
pub(super) fn set_loading_state(&self, loading_state: LoadingState) { |
||||
if self.loading_state.get() == loading_state { |
||||
return; |
||||
} |
||||
|
||||
self.loading_state.set(loading_state); |
||||
self.obj().notify_loading_state(); |
||||
} |
||||
|
||||
/// Update the room data with the given response.
|
||||
pub(super) fn update_data(&self, data: SpaceHierarchyRoomsChunk) { |
||||
self.set_alias(data.canonical_alias); |
||||
self.set_name(data.name); |
||||
self.set_topic(data.topic); |
||||
self.set_joined_members_count(data.num_joined_members.try_into().unwrap_or(u32::MAX)); |
||||
|
||||
if let Some(image) = self.obj().avatar_data().image() { |
||||
image.set_uri_and_info(data.avatar_url, None); |
||||
} |
||||
|
||||
self.set_loading_state(LoadingState::Ready); |
||||
} |
||||
|
||||
/// Load the data of this room.
|
||||
async fn load(&self) { |
||||
let Some(session) = self.session.upgrade() else { |
||||
return; |
||||
}; |
||||
|
||||
self.set_loading_state(LoadingState::Loading); |
||||
|
||||
let uri = self.uri(); |
||||
let client = session.client(); |
||||
|
||||
let room_id = match OwnedRoomId::try_from(uri.id.clone()) { |
||||
Ok(room_id) => room_id, |
||||
Err(alias) => { |
||||
let client_clone = client.clone(); |
||||
let handle = |
||||
spawn_tokio!(async move { client_clone.resolve_room_alias(&alias).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(response) => response.room_id, |
||||
Err(error) => { |
||||
warn!("Could not resolve room alias `{}`: {error}", uri.id); |
||||
self.set_loading_state(LoadingState::Error); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// FIXME: The space hierarchy endpoint gives us the room details we want, but it
|
||||
// doesn't work if the room is not known by the homeserver. We need MSC3266 for
|
||||
// a proper endpoint.
|
||||
let request = assign!(get_hierarchy::v1::Request::new(room_id.clone()), { |
||||
// We are only interested in the single room.
|
||||
limit: Some(uint!(1)) |
||||
}); |
||||
let handle = spawn_tokio!(async move { client.send(request).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(response) => { |
||||
if let Some(chunk) = response |
||||
.rooms |
||||
.into_iter() |
||||
.next() |
||||
.filter(|c| c.room_id == room_id) |
||||
{ |
||||
self.update_data(chunk); |
||||
} else { |
||||
debug!("Endpoint did not return requested room"); |
||||
self.set_loading_state(LoadingState::Error); |
||||
} |
||||
} |
||||
Err(error) => { |
||||
warn!("Could not get room details for room `{}`: {error}", uri.id); |
||||
self.set_loading_state(LoadingState::Error); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A Room that can only be updated by making remote calls, i.e. it won't be updated via sync.
|
||||
pub struct RemoteRoom(ObjectSubclass<imp::RemoteRoom>) |
||||
@extends PillSource; |
||||
} |
||||
|
||||
impl RemoteRoom { |
||||
pub(crate) fn new(session: &Session, uri: MatrixRoomIdUri) -> Self { |
||||
let obj = glib::Object::builder::<Self>() |
||||
.property("session", session) |
||||
.build(); |
||||
obj.imp().set_uri(uri); |
||||
obj |
||||
} |
||||
|
||||
/// The Matrix URI of this room.
|
||||
pub(crate) fn uri(&self) -> &MatrixRoomIdUri { |
||||
self.imp().uri() |
||||
} |
||||
|
||||
/// The canonical alias of this room.
|
||||
pub(crate) fn alias(&self) -> Option<OwnedRoomAliasId> { |
||||
self.imp().alias() |
||||
} |
||||
} |
||||
@ -1,67 +0,0 @@
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*}; |
||||
use matrix_sdk::ruma::OwnedUserId; |
||||
use tracing::error; |
||||
|
||||
use super::{Session, User}; |
||||
use crate::{components::PillSource, prelude::*, spawn_tokio}; |
||||
|
||||
mod imp { |
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default)] |
||||
pub struct RemoteUser {} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for RemoteUser { |
||||
const NAME: &'static str = "RemoteUser"; |
||||
type Type = super::RemoteUser; |
||||
type ParentType = User; |
||||
} |
||||
|
||||
impl ObjectImpl for RemoteUser {} |
||||
|
||||
impl PillSourceImpl for RemoteUser { |
||||
fn identifier(&self) -> String { |
||||
self.obj().upcast_ref::<User>().user_id_string() |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A User that can only be updated by making remote calls, i.e. it won't be updated via sync.
|
||||
pub struct RemoteUser(ObjectSubclass<imp::RemoteUser>) @extends PillSource, User; |
||||
} |
||||
|
||||
impl RemoteUser { |
||||
pub fn new(session: &Session, user_id: OwnedUserId) -> Self { |
||||
let obj = glib::Object::builder::<Self>() |
||||
.property("session", session) |
||||
.build(); |
||||
|
||||
obj.upcast_ref::<User>().imp().set_user_id(user_id); |
||||
obj |
||||
} |
||||
|
||||
/// Request this user's profile from the homeserver.
|
||||
pub async fn load_profile(&self) { |
||||
let user_id = self.user_id(); |
||||
|
||||
let client = self.session().client(); |
||||
let user_id_clone = user_id.clone(); |
||||
let handle = |
||||
spawn_tokio!( |
||||
async move { client.account().fetch_user_profile_of(&user_id_clone).await } |
||||
); |
||||
|
||||
let profile = match handle.await.unwrap() { |
||||
Ok(profile) => profile, |
||||
Err(error) => { |
||||
error!("Could not load profile for user `{user_id}`: {error}"); |
||||
return; |
||||
} |
||||
}; |
||||
|
||||
self.set_name(profile.displayname); |
||||
self.set_avatar_url(profile.avatar_url); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue