You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

300 lines
9.3 KiB

use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
use matrix_sdk::{
encryption::identities::UserIdentity,
ruma::{OwnedMxcUri, OwnedUserId, UserId},
};
use tracing::error;
use crate::{
components::Pill,
session::model::{
AvatarData, AvatarImage, AvatarUriSource, IdentityVerification, Session, VerificationState,
},
spawn, spawn_tokio,
};
#[glib::flags(name = "UserActions")]
pub enum UserActions {
VERIFY = 0b00000001,
}
impl Default for UserActions {
fn default() -> Self {
Self::empty()
}
}
mod imp {
use std::cell::{Cell, RefCell};
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default)]
pub struct User {
pub user_id: OnceCell<OwnedUserId>,
pub display_name: RefCell<Option<String>>,
pub session: OnceCell<Session>,
pub avatar_data: OnceCell<AvatarData>,
pub is_verified: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for User {
const NAME: &'static str = "User";
type Type = super::User;
}
impl ObjectImpl for User {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("user-id")
.construct_only()
.build(),
glib::ParamSpecString::builder("display-name")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<AvatarData>("avatar-data")
.read_only()
.build(),
glib::ParamSpecObject::builder::<Session>("session")
.construct_only()
.build(),
glib::ParamSpecBoolean::builder("verified")
.read_only()
.build(),
glib::ParamSpecFlags::builder::<UserActions>("allowed-actions")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"user-id" => {
self.user_id
.set(UserId::parse(value.get::<&str>().unwrap()).unwrap())
.unwrap();
}
"display-name" => {
self.obj().set_display_name(value.get().unwrap());
}
"session" => {
if let Some(session) = value.get().unwrap() {
if self.session.set(session).is_err() {
error!("Trying to set a session while it is already set");
}
}
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"display-name" => obj.display_name().to_value(),
"user-id" => obj.user_id().as_str().to_value(),
"session" => obj.session().to_value(),
"avatar-data" => obj.avatar_data().to_value(),
"verified" => obj.is_verified().to_value(),
"allowed-actions" => obj.allowed_actions().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
let avatar_data = AvatarData::with_image(AvatarImage::new(
obj.session(),
None,
AvatarUriSource::User,
));
self.avatar_data.set(avatar_data).unwrap();
obj.bind_property("display-name", obj.avatar_data(), "display-name")
.sync_create()
.build();
obj.init_is_verified();
}
}
}
glib::wrapper! {
/// `glib::Object` representation of a Matrix user.
pub struct User(ObjectSubclass<imp::User>);
}
impl User {
pub fn new(session: &Session, user_id: &UserId) -> Self {
glib::Object::builder()
.property("session", session)
.property("user-id", user_id.as_str())
.build()
}
pub async fn crypto_identity(&self) -> Option<UserIdentity> {
let encryption = self.session().client().encryption();
let user_id = self.user_id();
let handle = spawn_tokio!(async move { encryption.get_user_identity(&user_id).await });
match handle.await.unwrap() {
Ok(identity) => identity,
Err(error) => {
error!("Failed to find crypto identity: {error}");
None
}
}
}
pub async fn verify_identity(&self) -> IdentityVerification {
let request = IdentityVerification::create(self.session(), Some(self)).await;
self.session().verification_list().add(request.clone());
// FIXME: actually listen to room events to get updates for verification state
request.connect_notify_local(
Some("state"),
clone!(@weak self as obj => move |request,_| {
if request.state() == VerificationState::Completed {
obj.init_is_verified();
}
}),
);
request
}
/// Whether this user has been verified.
pub fn is_verified(&self) -> bool {
self.imp().is_verified.get()
}
fn init_is_verified(&self) {
spawn!(clone!(@weak self as obj => async move {
let is_verified = obj.crypto_identity().await.map_or(false, |i| i.is_verified());
if is_verified == obj.is_verified() {
return;
}
obj.imp().is_verified.set(is_verified);
obj.notify("verified");
obj.notify("allowed-actions");
}));
}
}
pub trait UserExt: IsA<User> {
/// The current session.
fn session(&self) -> &Session {
self.upcast_ref().imp().session.get().unwrap()
}
/// The ID of this user.
fn user_id(&self) -> OwnedUserId {
self.upcast_ref().imp().user_id.get().unwrap().clone()
}
/// The display name of this user.
fn display_name(&self) -> String {
let imp = self.upcast_ref().imp();
if let Some(display_name) = imp.display_name.borrow().to_owned() {
display_name
} else {
imp.user_id.get().unwrap().localpart().to_owned()
}
}
/// Set the display name of this user.
fn set_display_name(&self, display_name: Option<String>) {
if Some(self.display_name()) == display_name {
return;
}
self.upcast_ref().imp().display_name.replace(display_name);
self.notify("display-name");
}
/// The [`AvatarData`] of this user.
fn avatar_data(&self) -> &AvatarData {
self.upcast_ref().imp().avatar_data.get().unwrap()
}
/// Set the avatar URL of this user.
fn set_avatar_url(&self, uri: Option<OwnedMxcUri>) {
self.avatar_data()
.image()
.unwrap()
.set_uri(uri.map(String::from));
}
/// The actions the currently logged-in user is allowed to perform on this
/// user.
fn allowed_actions(&self) -> UserActions {
let user = self.upcast_ref();
let is_other = self.session().user().map_or(false, |session_user| {
session_user.user_id() != self.user_id()
});
if !user.is_verified() && is_other {
UserActions::VERIFY
} else {
UserActions::empty()
}
}
/// Get a `Pill` representing this `User`.
fn to_pill(&self) -> Pill {
let user = self.upcast_ref();
Pill::for_user(user)
}
/// Get the HTML mention representation for this `User`.
fn html_mention(&self) -> String {
let uri = self.user_id().matrix_to_uri();
format!("<a href=\"{uri}\">{}</a>", self.display_name())
}
/// Load the user profile from the homeserver.
///
/// This overwrites the already loaded display name and avatar.
fn load_profile(&self) {
let client = self.session().client();
let user_id = self.user_id();
let user = self.upcast_ref::<User>();
let handle = spawn_tokio!(async move { client.get_profile(&user_id).await });
spawn!(clone!(@weak user => async move {
match handle.await.unwrap() {
Ok(response) => {
user.set_display_name(response.displayname);
user.set_avatar_url(response.avatar_url);
},
Err(error) => {
error!("Failed to load user profile for {}: {}", user.user_id(), error);
}
};
}));
}
}
impl<T: IsA<User>> UserExt for T {}
unsafe impl<T: ObjectImpl + 'static> IsSubclassable<T> for User {
fn class_init(class: &mut glib::Class<Self>) {
<glib::Object as IsSubclassable<T>>::class_init(class.upcast_ref_mut());
}
fn instance_init(instance: &mut glib::subclass::InitializingObject<T>) {
<glib::Object as IsSubclassable<T>>::instance_init(instance);
}
}