// xmpp-client: A sample GTK client in Rust // Copyright (C) 2024 Link Mauve // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use adw::prelude::*; use gtk::{gio, glib}; mod message; mod poezio_logs; mod tab; mod window; mod xmpp_client; use message::Message; use tab::Tab; fn get_own_nick() -> String { std::env::var("USER").unwrap() } fn xep_0392(input: &str) -> String { use sha1::Digest; let sha1 = sha1::Sha1::digest(input); let hue = ((sha1[1] as u16) << 8 | sha1[0] as u16) as f64 / 65536. * 360.; let (r, g, b) = hsluv::hsluv_to_rgb(hue, 100., 75.); let (r, g, b) = ((r * 255.) as u8, (g * 255.) as u8, (b * 255.) as u8); format!("#{:02x}{:02x}{:02x}", r, g, b) } fn main() { 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(); let tabs_store = gio::ListStore::new::(); let (xmpp_receiver, cmd_sender) = xmpp_client::client(&username, &password); let tabs_store_copy = tabs_store.clone(); glib::spawn_future_local(async move { while let Ok(event) = xmpp_receiver.recv().await { match event { xmpp::Event::ContactAdded(jid) => { let tab = Tab::new(jid.jid.as_str(), jid.jid.as_str()); tabs_store_copy.append(&tab); } xmpp::Event::ChatMessage(_id, _from, _body, _time) => { // TODO: Insert message into tab history continue; } _ => continue, } } }); app.connect_startup(move |app| { let win = window::MainWindow::new(app); let action_close = gio::ActionEntry::builder("close") .activate(|window: &window::MainWindow, _, _| { window.close(); }) .build(); win.add_action_entries([action_close]); app.set_accels_for_action("win.close", &["Q"]); assert!(Tab::static_type().is_valid()); assert!(Message::static_type().is_valid()); let store = gio::ListStore::new::(); let factory = gtk::SignalListItemFactory::new(); factory.connect_setup(move |_, list_item| { let label = gtk::Label::new(None); label.set_halign(gtk::Align::Start); label.set_use_markup(true); let list_item: >k::ListItem = list_item.downcast_ref().unwrap(); list_item.set_child(Some(&label)); }); factory.connect_bind(move |_, list_item| { let list_item: >k::ListItem = list_item.downcast_ref().unwrap(); let message: Message = list_item.item().and_downcast().unwrap(); let label: gtk::Label = list_item.child().and_downcast().unwrap(); let date = message.date(); let date = if date.ymd() == glib::DateTime::now(&glib::TimeZone::local()).unwrap().ymd() { date.format("%H:%M:%S").unwrap() } else { date.format("%Y-%m-%d %H:%M:%S").unwrap() }; let author = message.author(); let color = xep_0392(&author); let body = message.body(); let body = html_escape::encode_text(&body); let string = format!("{date} {author}> {body}"); label.set_label(&string); }); win.messages().set_factory(Some(&factory)); win.selection().set_model(Some(&store)); let tabs_factory = gtk::SignalListItemFactory::new(); tabs_factory.connect_setup(move |_, list_item| { let label = gtk::Label::new(None); label.set_halign(gtk::Align::Start); let list_item: >k::ListItem = list_item.downcast_ref().unwrap(); list_item.set_child(Some(&label)); }); tabs_factory.connect_bind(move |_, list_item| { let list_item: >k::ListItem = list_item.downcast_ref().unwrap(); let tab: Tab = list_item.item().and_downcast().unwrap(); let label: gtk::Label = list_item.child().and_downcast().unwrap(); label.set_label(&tab.name()); label.set_tooltip_text(Some(&tab.jid())); }); win.tabs().set_factory(Some(&tabs_factory)); win.tabs_selection().set_model(Some(&tabs_store)); let win2 = win.clone(); win.tabs_selection() .connect_selection_changed(move |tabs_selection, _, _| { let item = tabs_selection.selected_item().unwrap(); let tab: &Tab = item.downcast_ref().unwrap(); println!("Switching to {}", tab.jid()); let store = poezio_logs::load_logs(&tab.jid()); let selection = win2.selection(); selection.set_model(Some(&store)); win2.messages().scroll_to( selection.n_items() - 1, gtk::ListScrollFlags::FOCUS, None, ); win2.split_view().set_show_content(true); win2.entry().grab_focus(); }); let win2 = win.clone(); let cmd_sender2 = cmd_sender.clone(); win.entry().connect_activate(move |entry| { let text = entry.text(); entry.set_text(""); println!("Send: {text}"); let current_tab: Tab = win2 .tabs_selection() .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 .model() .unwrap() .downcast::() .unwrap(); store.append(&message); win2.messages() .scroll_to(selection.n_items() - 1, gtk::ListScrollFlags::FOCUS, None); }); win.messages().scroll_to( win.selection().n_items() - 1, gtk::ListScrollFlags::FOCUS, None, ); win.present(); }); app.run(); }