Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60c22a282e | |||
| c9323b38de | |||
| d5af178c91 |
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
/data
|
||||
|
||||
+2
-2
@@ -15,6 +15,6 @@ hsluv = "0.3.1"
|
||||
sha1 = "0.10.6"
|
||||
tokio = { version = "1", features = [ "rt" ] }
|
||||
xmpp = { git = "https://gitlab.com/xmpp-rs/xmpp-rs" }
|
||||
tokio-xmpp = { git = "https://gitlab.com/xmpp-rs/xmpp-rs", features = ["syntax-highlighting"] }
|
||||
async-channel = "2.3.1"
|
||||
env_logger = { version = "0.11.3", default-features = false, features = ["color", "auto-color", "humantime"] }
|
||||
pretty_env_logger = "0.5"
|
||||
log = "0.4"
|
||||
|
||||
+51
-42
@@ -15,6 +15,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::ObjectSubclassIsExt;
|
||||
use gtk::{gio, glib};
|
||||
|
||||
mod message;
|
||||
@@ -40,45 +41,51 @@ fn xep_0392(input: &str) -> String {
|
||||
format!("#{:02x}{:02x}{:02x}", r, g, b)
|
||||
}
|
||||
|
||||
fn on_login_pressed(win: &window::MainWindow) {
|
||||
let jid = win.jid().text();
|
||||
let password = win.password().text();
|
||||
println!("jid={jid:?} password={password:?}");
|
||||
|
||||
if !jid.is_empty() && !password.is_empty() {
|
||||
win.stack().set_visible_child(win.spinner());
|
||||
|
||||
let (xmpp_receiver, cmd_sender) = xmpp_client::client(&jid, &password);
|
||||
let win2 = win.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
while let Ok(event) = xmpp_receiver.recv().await {
|
||||
match event {
|
||||
xmpp::Event::Online => {
|
||||
win2.stack().set_visible_child(win2.split_view());
|
||||
}
|
||||
xmpp::Event::ContactAdded(jid) => {
|
||||
let tab = Tab::new(jid.jid.as_str(), jid.jid.as_str());
|
||||
win2.tabs_store().append(&tab);
|
||||
}
|
||||
xmpp::Event::ChatMessage(_id, _from, _body, _time) => {
|
||||
// TODO: Insert message into tab history
|
||||
continue;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
pretty_env_logger::init();
|
||||
|
||||
let mut args = std::env::args();
|
||||
let _ = args.next().unwrap();
|
||||
let username = args.next().expect("Please give username argument 1");
|
||||
let password = args.next().expect("Please give password argument 2");
|
||||
|
||||
let app = adw::Application::builder()
|
||||
.application_id("fr.linkmauve.XmppClient")
|
||||
.flags(gio::ApplicationFlags::HANDLES_COMMAND_LINE)
|
||||
.build();
|
||||
|
||||
app.connect_activate(move |app| {
|
||||
let tabs_store = gio::ListStore::new::<Tab>();
|
||||
let tabs_store_copy = tabs_store.clone();
|
||||
let (xmpp_receiver, cmd_sender) = xmpp_client::client(&username, &password);
|
||||
glib::spawn_future_local(async move {
|
||||
while let Ok(event) = xmpp_receiver.recv().await {
|
||||
match event {
|
||||
xmpp_client::XMPPEvent::Avatar(jid, avatar) => {
|
||||
log::info!("AVATAR");
|
||||
if let Some(tab) = tabs_store_copy.iter().find(|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_client::XMPPEvent::Contact(jid, avatar) => {
|
||||
log::info!("CONTACT");
|
||||
let tab = Tab::new(jid.as_str(), jid.as_str());
|
||||
|
||||
if let Some(avatar) = avatar {
|
||||
tab.set_avatar_hash(avatar);
|
||||
}
|
||||
|
||||
tabs_store_copy.append(&tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.connect_startup(move |app| {
|
||||
let win = window::MainWindow::new(app);
|
||||
|
||||
let action_close = gio::ActionEntry::builder("close")
|
||||
@@ -89,11 +96,6 @@ fn main() {
|
||||
win.add_action_entries([action_close]);
|
||||
app.set_accels_for_action("win.close", &["<Ctrl>Q"]);
|
||||
|
||||
let win2 = win.clone();
|
||||
win.login().connect_clicked(move |_| {
|
||||
on_login_pressed(&win2);
|
||||
});
|
||||
|
||||
assert!(Tab::static_type().is_valid());
|
||||
assert!(Message::static_type().is_valid());
|
||||
|
||||
@@ -137,16 +139,25 @@ fn main() {
|
||||
list_item.set_child(Some(&tab_widget));
|
||||
});
|
||||
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: >k::ListItem = list_item.downcast_ref().unwrap();
|
||||
let tab: Tab = list_item.item().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_jid(&tab.jid());
|
||||
});
|
||||
|
||||
win.tabs().set_factory(Some(&tabs_factory));
|
||||
win.tabs_selection().set_model(Some(win.tabs_store()));
|
||||
win.tabs_selection().set_model(Some(&tabs_store));
|
||||
|
||||
let win2 = win.clone();
|
||||
win.tabs_selection()
|
||||
@@ -167,7 +178,7 @@ fn main() {
|
||||
});
|
||||
|
||||
let win2 = win.clone();
|
||||
//let cmd_sender2 = cmd_sender.clone();
|
||||
let cmd_sender2 = cmd_sender.clone();
|
||||
win.entry().connect_activate(move |entry| {
|
||||
let text = entry.text();
|
||||
entry.set_text("");
|
||||
@@ -177,14 +188,12 @@ fn main() {
|
||||
.selected_item()
|
||||
.and_downcast()
|
||||
.unwrap();
|
||||
/*
|
||||
cmd_sender2
|
||||
.send_blocking(xmpp_client::XMPPCommand::SendPM(
|
||||
xmpp::BareJid::new(¤t_tab.jid()).unwrap(),
|
||||
text.as_str().to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
*/
|
||||
let message = Message::now(&get_own_nick(), &text);
|
||||
let selection = win2.selection();
|
||||
let store = selection
|
||||
|
||||
+3
-2
@@ -25,8 +25,8 @@ mod imp {
|
||||
#[derive(glib::Properties)]
|
||||
#[properties(wrapper_type = super::Tab)]
|
||||
pub struct Tab {
|
||||
//#[property(get, set)]
|
||||
//avatar: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
avatar_hash: RefCell<Option<String>>,
|
||||
#[property(get, construct_only)]
|
||||
jid: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
@@ -36,6 +36,7 @@ mod imp {
|
||||
impl Default for Tab {
|
||||
fn default() -> Self {
|
||||
Tab {
|
||||
avatar_hash: RefCell::new(None),
|
||||
jid: RefCell::new(String::new()),
|
||||
name: RefCell::new(String::new()),
|
||||
}
|
||||
|
||||
+12
-4
@@ -68,13 +68,21 @@ impl ChatTab {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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) {
|
||||
self.imp().name.set_label(name);
|
||||
}
|
||||
|
||||
+1
-22
@@ -15,33 +15,12 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{gio, glib};
|
||||
use gtk::glib;
|
||||
|
||||
/// The private struct, which can hold widgets and other data.
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
||||
#[template(file = "window.ui")]
|
||||
pub struct MainWindow {
|
||||
#[template_child]
|
||||
pub stack: TemplateChild<gtk::Stack>,
|
||||
|
||||
// For the login page
|
||||
|
||||
#[template_child]
|
||||
pub jid: TemplateChild<adw::EntryRow>,
|
||||
#[template_child]
|
||||
pub password: TemplateChild<adw::PasswordEntryRow>,
|
||||
#[template_child]
|
||||
pub login: TemplateChild<gtk::Button>,
|
||||
|
||||
// For the spinner
|
||||
|
||||
#[template_child]
|
||||
pub spinner: TemplateChild<gtk::Box>,
|
||||
|
||||
// For the chats page
|
||||
|
||||
#[template_child]
|
||||
pub tabs_store: TemplateChild<gio::ListStore>,
|
||||
#[template_child]
|
||||
pub split_view: TemplateChild<adw::NavigationSplitView>,
|
||||
#[template_child]
|
||||
|
||||
@@ -30,30 +30,6 @@ impl MainWindow {
|
||||
glib::Object::builder().property("application", app).build()
|
||||
}
|
||||
|
||||
pub fn stack(&self) -> >k::Stack {
|
||||
&self.imp().stack
|
||||
}
|
||||
|
||||
pub fn jid(&self) -> &adw::EntryRow {
|
||||
&self.imp().jid
|
||||
}
|
||||
|
||||
pub fn password(&self) -> &adw::PasswordEntryRow {
|
||||
&self.imp().password
|
||||
}
|
||||
|
||||
pub fn login(&self) -> >k::Button {
|
||||
&self.imp().login
|
||||
}
|
||||
|
||||
pub fn spinner(&self) -> >k::Box {
|
||||
&self.imp().spinner
|
||||
}
|
||||
|
||||
pub fn tabs_store(&self) -> &gio::ListStore {
|
||||
&self.imp().tabs_store
|
||||
}
|
||||
|
||||
pub fn split_view(&self) -> &adw::NavigationSplitView {
|
||||
&self.imp().split_view
|
||||
}
|
||||
|
||||
@@ -13,68 +13,6 @@
|
||||
</object>
|
||||
</child>
|
||||
<property name="content">
|
||||
<object class="GtkStack" id="stack">
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Enter your XMPP credentials</property>
|
||||
<child>
|
||||
<object class="AdwEntryRow" id="jid">
|
||||
<property name="title" translatable="yes">JID</property>
|
||||
<property name="input-purpose">email</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPasswordEntryRow" id="password">
|
||||
<property name="title" translatable="yes">Password</property>
|
||||
<property name="input-purpose">password</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="login">
|
||||
<property name="label" translatable="yes">Login</property>
|
||||
<property name="css-classes">suggested-action</property>
|
||||
<property name="margin-top">8</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="spinner">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">Connecting…</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="spinning">yes</property>
|
||||
<property name="vexpand">no</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwNavigationSplitView" id="split_view">
|
||||
<property name="sidebar">
|
||||
<object class="AdwNavigationPage">
|
||||
@@ -130,7 +68,6 @@
|
||||
<object class="GtkEntry" id="entry">
|
||||
<property name="hexpand">yes</property>
|
||||
<property name="placeholder-text" translatable="yes">Send a message</property>
|
||||
<property name="input-purpose">free-form</property>
|
||||
</object>
|
||||
</child>
|
||||
<!--
|
||||
@@ -149,11 +86,8 @@
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
<object class="GListStore" id="tabs_store"/>
|
||||
<object class="GtkSingleSelection" id="tabs_selection"/>
|
||||
<object class="GtkNoSelection" id="selection"/>
|
||||
</interface>
|
||||
|
||||
+73
-19
@@ -1,7 +1,8 @@
|
||||
use async_channel::{Receiver, Sender};
|
||||
use tokio::runtime::Runtime;
|
||||
use xmpp::{BareJid, ClientBuilder, ClientFeature, Event};
|
||||
use xmpp::{BareJid, ClientBuilder, ClientFeature};
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -9,13 +10,76 @@ pub enum XMPPCommand {
|
||||
SendPM(BareJid, String),
|
||||
}
|
||||
|
||||
fn tokio_runtime() -> &'static Runtime {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum XMPPEvent {
|
||||
Avatar(BareJid, String),
|
||||
Contact(BareJid, Option<String>),
|
||||
}
|
||||
|
||||
pub fn tokio_runtime() -> &'static Runtime {
|
||||
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
|
||||
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>) {
|
||||
let (event_sender, event_receiver) = async_channel::bounded::<xmpp::Event>(1);
|
||||
pub async fn handle_xmpp_event(event: xmpp::Event) -> Option<XMPPEvent> {
|
||||
match event {
|
||||
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
|
||||
log::info!("avatar Already have latest link {jid} to hash {hash}");
|
||||
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());
|
||||
log::info!("Found existing avatar for contact: {avatar_jid}");
|
||||
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 jid = jid.to_string();
|
||||
@@ -24,6 +88,7 @@ pub(crate) fn client(jid: &str, password: &str) -> (Receiver<xmpp::Event>, Sende
|
||||
tokio_runtime().spawn(async move {
|
||||
let mut client = ClientBuilder::new(BareJid::new(&jid).unwrap(), &password)
|
||||
.set_default_nick("xmpp-client")
|
||||
.enable_feature(ClientFeature::Avatars)
|
||||
.enable_feature(ClientFeature::ContactList)
|
||||
.build();
|
||||
|
||||
@@ -31,25 +96,14 @@ pub(crate) fn client(jid: &str, password: &str) -> (Receiver<xmpp::Event>, Sende
|
||||
tokio::select! {
|
||||
Some(events) = client.wait_for_events() => {
|
||||
for event in events {
|
||||
match event {
|
||||
Event::Online => {
|
||||
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;
|
||||
}
|
||||
if let Some(parsed_event) = handle_xmpp_event(event).await {
|
||||
let _ = event_sender.send(parsed_event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(command) = cmd_receiver.recv() => {
|
||||
match command {
|
||||
command = cmd_receiver.recv() => {
|
||||
match command.unwrap() {
|
||||
XMPPCommand::SendPM(jid, content) => {
|
||||
client.send_message(jid.into(), xmpp::parsers::message::MessageType::Chat, "en", &content).await;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user