Load avatars from xmpp

This commit is contained in:
xmppftw xmppftw 2024-06-04 19:20:54 +02:00
parent c77141db1a
commit fe09a79d2c
4 changed files with 119 additions and 36 deletions

View File

@ -41,31 +41,40 @@ fn xep_0392(input: &str) -> String {
format!("#{:02x}{:02x}{:02x}", r, g, b) format!("#{:02x}{:02x}{:02x}", r, g, b)
} }
fn on_login_pressed(win: &window::MainWindow) { fn on_login_pressed(win: window::MainWindow) {
let jid = win.jid().text(); let jid = win.jid().text();
let password = win.password().text(); let password = win.password().text();
println!("jid={jid:?} password={password:?}");
if !jid.is_empty() && !password.is_empty() { if !jid.is_empty() && !password.is_empty() {
win.stack().set_visible_child(win.spinner()); win.stack().set_visible_child(win.spinner());
let (xmpp_receiver, cmd_sender) = xmpp_client::client(&jid, &password); let (xmpp_receiver, cmd_sender) = xmpp_client::client(&jid, &password);
let win2 = win.clone(); // let win2 = win.clone();
glib::spawn_future_local(async move { glib::spawn_future_local(async move {
while let Ok(event) = xmpp_receiver.recv().await { while let Ok(event) = xmpp_receiver.recv().await {
match event { match event {
xmpp::Event::Online => { xmpp_client::XMPPEvent::Online => {
win2.stack().set_visible_child(win2.split_view()); win.stack().set_visible_child(win.split_view());
} }
xmpp::Event::ContactAdded(jid) => { xmpp_client::XMPPEvent::Avatar(jid, avatar) => {
let tab = Tab::new(jid.jid.as_str(), jid.jid.as_str()); if let Some(tab) = win.tabs_store().iter().find(|tab| {
win2.tabs_store().append(&tab); let tab: &glib::object::Object = tab.as_ref().unwrap();
let tab: &Tab = tab.downcast_ref().unwrap();
tab.jid() == jid.as_str()
}) {
let tab: &Tab = tab.as_ref().unwrap().downcast_ref().unwrap();
tab.set_avatar_hash(avatar.clone());
}
} }
xmpp::Event::ChatMessage(_id, _from, _body, _time) => { xmpp_client::XMPPEvent::Contact(jid, avatar) => {
// TODO: Insert message into tab history let tab = Tab::new(jid.as_str(), jid.as_str());
continue;
if let Some(avatar) = avatar {
tab.set_avatar_hash(avatar);
}
win.tabs_store().append(&tab);
} }
_ => continue,
} }
} }
}); });
@ -109,7 +118,7 @@ fn main() {
let win2 = win.clone(); let win2 = win.clone();
win.login().connect_clicked(move |_| { win.login().connect_clicked(move |_| {
on_login_pressed(&win2); on_login_pressed(win2.clone());
}); });
assert!(Tab::static_type().is_valid()); assert!(Tab::static_type().is_valid());
@ -155,9 +164,18 @@ fn main() {
list_item.set_child(Some(&tab_widget)); list_item.set_child(Some(&tab_widget));
}); });
tabs_factory.connect_bind(move |_, list_item| { tabs_factory.connect_bind(move |_, list_item| {
// executed when switch tab, one time for previous tab and one time for new tab
// but also when create new tab
let list_item: &gtk::ListItem = list_item.downcast_ref().unwrap(); let list_item: &gtk::ListItem = list_item.downcast_ref().unwrap();
let tab: Tab = list_item.item().and_downcast().unwrap(); let tab: Tab = list_item.item().and_downcast().unwrap();
let tab_widget: widgets::ChatTab = list_item.child().and_downcast().unwrap(); let tab_widget: widgets::ChatTab = list_item.child().and_downcast().unwrap();
tab.bind_property(
"avatar_hash",
&tab_widget.imp().avatar.try_get().unwrap(),
"file",
)
.sync_create()
.build();
tab_widget.set_name(&tab.name()); tab_widget.set_name(&tab.name());
tab_widget.set_jid(&tab.jid()); tab_widget.set_jid(&tab.jid());

View File

@ -25,8 +25,8 @@ mod imp {
#[derive(glib::Properties)] #[derive(glib::Properties)]
#[properties(wrapper_type = super::Tab)] #[properties(wrapper_type = super::Tab)]
pub struct Tab { pub struct Tab {
//#[property(get, set)] #[property(get, set)]
//avatar: RefCell<String>, avatar_hash: RefCell<Option<String>>,
#[property(get, construct_only)] #[property(get, construct_only)]
jid: RefCell<String>, jid: RefCell<String>,
#[property(get, set)] #[property(get, set)]
@ -36,6 +36,7 @@ mod imp {
impl Default for Tab { impl Default for Tab {
fn default() -> Self { fn default() -> Self {
Tab { Tab {
avatar_hash: RefCell::new(None),
jid: RefCell::new(String::new()), jid: RefCell::new(String::new()),
name: RefCell::new(String::new()), name: RefCell::new(String::new()),
} }

View File

@ -68,13 +68,21 @@ impl ChatTab {
} }
pub fn set_jid(&self, jid: &str) { pub fn set_jid(&self, jid: &str) {
let hash = "123456789abcdef123456789abcdef123456789a";
self.imp().avatar.set_from_file(Some(format!(
"/home/linkmauve/cache/poezio/avatars/{jid}/{hash}"
)));
self.set_tooltip_text(Some(jid)); self.set_tooltip_text(Some(jid));
} }
pub fn set_avatar_hash(&self, hash: &str) {
// let xdg_cache = std::env::var("XDG_CACHE_HOME").unwrap_or(format!(
// "{}/.cache",
// std::env::var("HOME").expect("NO $HOME?!")
// ));
// let xdg_cache = PathBuf::from(xdg_cache);
// let entry = xdg_cache.join("poezio/avatars").join(self.tooltip_text().unwrap()).join(hash);
// self.imp().avatar.set_from_file(Some(entry.to_str().unwrap().to_string()));
self.imp().avatar.set_from_file(Some(hash));
}
pub fn set_name(&self, name: &str) { pub fn set_name(&self, name: &str) {
self.imp().name.set_label(name); self.imp().name.set_label(name);
} }

View File

@ -1,7 +1,8 @@
use async_channel::{Receiver, Sender}; use async_channel::{Receiver, Sender};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use xmpp::{BareJid, ClientBuilder, ClientFeature, Event}; use xmpp::{BareJid, ClientBuilder, ClientFeature};
use std::path::PathBuf;
use std::sync::OnceLock; use std::sync::OnceLock;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -9,13 +10,78 @@ pub enum XMPPCommand {
SendPM(BareJid, String), SendPM(BareJid, String),
} }
fn tokio_runtime() -> &'static Runtime { #[derive(Clone, Debug)]
pub enum XMPPEvent {
Online,
Avatar(BareJid, String),
Contact(BareJid, Option<String>),
}
pub fn tokio_runtime() -> &'static Runtime {
static RUNTIME: OnceLock<Runtime> = OnceLock::new(); static RUNTIME: OnceLock<Runtime> = OnceLock::new();
RUNTIME.get_or_init(|| Runtime::new().expect("Setting up tokio runtime needs to succeed.")) RUNTIME.get_or_init(|| Runtime::new().expect("Setting up tokio runtime needs to succeed."))
} }
pub(crate) fn client(jid: &str, password: &str) -> (Receiver<xmpp::Event>, Sender<XMPPCommand>) { pub async fn handle_xmpp_event(event: xmpp::Event) -> Option<XMPPEvent> {
let (event_sender, event_receiver) = async_channel::bounded::<xmpp::Event>(1); match event {
xmpp::Event::Online => {
Some(XMPPEvent::Online)
}
xmpp::Event::AvatarRetrieved(jid, hash) => {
// need update latest symlink?
// xmpp-rs stores avatar in data/JID/HASH... we create data/JID/latest symlink points to relative HASH
let mut latest_link = PathBuf::from(&hash);
let hash_name = latest_link
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string();
latest_link.set_file_name("latest");
if let Ok(metadata) = tokio::fs::metadata(&latest_link).await {
if metadata.is_symlink() {
if let Ok(previous_latest) = tokio::fs::read_link(&latest_link).await {
if previous_latest == PathBuf::from(&hash_name) {
// We already have latest symlink
return None;
}
}
}
}
// Setting new latest symlink
let _ = tokio::fs::symlink(hash_name, latest_link).await;
if jid.is_bare() {
Some(XMPPEvent::Avatar(jid.to_bare(), hash))
} else {
// no avatar for FullJid (yet)
None
}
}
xmpp::Event::ContactAdded(jid) => {
let avatar_jid = jid.jid.clone();
if let Ok(hash_name) =
tokio::fs::read_link(&format!("data/{}/latest", &avatar_jid)).await
{
let avatar = format!("data/{}/{}", &avatar_jid, hash_name.display());
Some(XMPPEvent::Contact(avatar_jid, Some(avatar)))
} else {
Some(XMPPEvent::Contact(avatar_jid, None))
}
}
xmpp::Event::ChatMessage(_id, _from, _body, _time) => {
// TODO: Insert message into tab history
None
}
_ => None,
}
}
pub(crate) fn client(jid: &str, password: &str) -> (Receiver<XMPPEvent>, Sender<XMPPCommand>) {
let (event_sender, event_receiver) = async_channel::bounded::<XMPPEvent>(1);
let (cmd_sender, cmd_receiver) = async_channel::bounded::<XMPPCommand>(1); let (cmd_sender, cmd_receiver) = async_channel::bounded::<XMPPCommand>(1);
let jid = jid.to_string(); let jid = jid.to_string();
@ -24,6 +90,7 @@ pub(crate) fn client(jid: &str, password: &str) -> (Receiver<xmpp::Event>, Sende
tokio_runtime().spawn(async move { tokio_runtime().spawn(async move {
let mut client = ClientBuilder::new(BareJid::new(&jid).unwrap(), &password) let mut client = ClientBuilder::new(BareJid::new(&jid).unwrap(), &password)
.set_default_nick("xmpp-client") .set_default_nick("xmpp-client")
.enable_feature(ClientFeature::Avatars)
.enable_feature(ClientFeature::ContactList) .enable_feature(ClientFeature::ContactList)
.build(); .build();
@ -31,19 +98,8 @@ pub(crate) fn client(jid: &str, password: &str) -> (Receiver<xmpp::Event>, Sende
tokio::select! { tokio::select! {
Some(events) = client.wait_for_events() => { Some(events) = client.wait_for_events() => {
for event in events { for event in events {
match event { if let Some(parsed_event) = handle_xmpp_event(event).await {
Event::Online => { let _ = event_sender.send(parsed_event).await;
event_sender.send(event).await.expect("BOOOOOOHOOOO");
}
Event::ContactAdded(_) => {
event_sender.send(event).await.expect("BOOOOOOHOOOO");
}
Event::ChatMessage(_, _, _, _) => {
event_sender.send(event).await.expect("BOOOHOOOO");
}
_ => {
continue;
}
} }
} }
} }