Compare commits
12 Commits
load-avata
...
main
Author | SHA1 | Date | |
---|---|---|---|
e7afaaa100 | |||
f9a8202b6b | |||
a05e33f4a4 | |||
fe09a79d2c | |||
c77141db1a | |||
786d9c462b | |||
|
81080238af | ||
|
29afa41437 | ||
|
5824275f06 | ||
|
690e7113ab | ||
|
f09a1cff2d | ||
|
7c85e6e006 |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
/data
|
||||||
|
|
|
@ -14,5 +14,9 @@ html-escape = "0.2.13"
|
||||||
hsluv = "0.3.1"
|
hsluv = "0.3.1"
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
tokio = { version = "1", features = [ "rt" ] }
|
tokio = { version = "1", features = [ "rt" ] }
|
||||||
xmpp = { git = "https://gitlab.com/xmpp-rs/xmpp-rs" }
|
xmpp = { git = "https://gitlab.com/xmpp-rs/xmpp-rs", features = [ "syntax-highlighting", "serde" ] }
|
||||||
async-channel = "2.3.1"
|
async-channel = "2.3.1"
|
||||||
|
env_logger = { version = "0.11.3", default-features = false, features = ["color", "auto-color", "humantime"] }
|
||||||
|
camino = "1.1"
|
||||||
|
serde = { version = "1.0", features = [ "derive" ] }
|
||||||
|
toml = "0.8"
|
||||||
|
|
83
src/config.rs
Normal file
83
src/config.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use xmpp::BareJid;
|
||||||
|
|
||||||
|
use crate::xdg::xdg_config;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct ConfigFile {
|
||||||
|
pub accounts: Vec<AccountConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AccountConfig {
|
||||||
|
jid: BareJid,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountConfig {
|
||||||
|
pub fn new(jid: &BareJid, password: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
jid: jid.clone(),
|
||||||
|
password: password.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigFile {
|
||||||
|
pub fn xdg_path() -> Utf8PathBuf {
|
||||||
|
xdg_config("rino").join("config.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_xdg() -> Self {
|
||||||
|
let config_path = Self::xdg_path();
|
||||||
|
if config_path.is_file() {
|
||||||
|
let content = std::fs::read_to_string(&config_path).unwrap();
|
||||||
|
toml::from_str(&content).unwrap()
|
||||||
|
} else {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_xdg(&self) {
|
||||||
|
let config_path = Self::xdg_path();
|
||||||
|
let config_dir = config_path.parent().unwrap();
|
||||||
|
if !config_dir.is_dir() {
|
||||||
|
std::fs::create_dir_all(&config_dir).unwrap();
|
||||||
|
}
|
||||||
|
std::fs::write(&config_path, toml::to_string(&self).unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_account_password(&self, jid: &BareJid) -> Option<&str> {
|
||||||
|
self.accounts
|
||||||
|
.iter()
|
||||||
|
.find(|acc| &acc.jid == jid)
|
||||||
|
.map(|x| x.password.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_account(&mut self, jid: &BareJid, password: &str) {
|
||||||
|
if let Some(p) = self.get_account_password(jid) {
|
||||||
|
if p == password {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for acc in self.accounts.iter_mut() {
|
||||||
|
if &acc.jid == jid {
|
||||||
|
acc.password = password.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let account = AccountConfig::new(jid, password);
|
||||||
|
self.accounts.push(account);
|
||||||
|
}
|
||||||
|
self.to_xdg();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_first_account(&self) -> Option<(&BareJid, &String)> {
|
||||||
|
for account in &self.accounts {
|
||||||
|
return Some((&account.jid, &account.password));
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
35
src/helpers/message_store.rs
Normal file
35
src/helpers/message_store.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use gtk::gio::ListStore;
|
||||||
|
use xmpp::BareJid;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use crate::poezio_logs::load_logs;
|
||||||
|
use crate::Message;
|
||||||
|
|
||||||
|
pub type InnerMessageStore = HashMap<BareJid, ListStore>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct MessageStore {
|
||||||
|
/// complete message history
|
||||||
|
store: Arc<RwLock<InnerMessageStore>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MessageStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_jid(&self, jid: &BareJid) -> ListStore {
|
||||||
|
self.store
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.entry(jid.clone())
|
||||||
|
.or_insert(load_logs(jid.as_str()))
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_message(&self, jid: &BareJid, message: &Message) {
|
||||||
|
self.with_jid(jid).append(message);
|
||||||
|
}
|
||||||
|
}
|
1
src/helpers/mod.rs
Normal file
1
src/helpers/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod message_store;
|
140
src/main.rs
140
src/main.rs
|
@ -15,15 +15,23 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
use adw::subclass::prelude::ObjectSubclassIsExt;
|
||||||
use gtk::{gio, glib};
|
use gtk::{gio, glib};
|
||||||
|
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod helpers;
|
||||||
mod message;
|
mod message;
|
||||||
mod poezio_logs;
|
mod poezio_logs;
|
||||||
mod tab;
|
mod tab;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
mod window;
|
mod window;
|
||||||
|
mod xdg;
|
||||||
mod xmpp_client;
|
mod xmpp_client;
|
||||||
|
|
||||||
|
use config::ConfigFile;
|
||||||
|
use helpers::message_store::MessageStore;
|
||||||
use message::Message;
|
use message::Message;
|
||||||
use tab::Tab;
|
use tab::Tab;
|
||||||
|
|
||||||
|
@ -40,39 +48,90 @@ 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,
|
||||||
|
config: Arc<RwLock<ConfigFile>>,
|
||||||
|
messages: MessageStore,
|
||||||
|
xmpp_cmd: xmpp_client::CommandSender,
|
||||||
|
) {
|
||||||
|
let jid = win.jid().text();
|
||||||
|
let password = win.password().text();
|
||||||
|
|
||||||
|
if !jid.is_empty() && !password.is_empty() {
|
||||||
|
win.stack().set_visible_child(win.spinner());
|
||||||
|
|
||||||
|
let (xmpp_receiver, cmd_sender) = xmpp_client::client(&jid, &password);
|
||||||
|
xmpp_cmd.set_sender(cmd_sender);
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
while let Ok(event) = xmpp_receiver.recv().await {
|
||||||
|
match event {
|
||||||
|
xmpp_client::XMPPEvent::Online => {
|
||||||
|
// Success login, save password
|
||||||
|
config
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.save_account(&xmpp::BareJid::new(jid.as_str()).unwrap(), &password);
|
||||||
|
win.stack().set_visible_child(win.split_view());
|
||||||
|
}
|
||||||
|
xmpp_client::XMPPEvent::Avatar(jid, avatar) => {
|
||||||
|
if let Some(tab) = win.tabs_store().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) => {
|
||||||
|
let tab = Tab::new(jid.as_str(), jid.as_str());
|
||||||
|
|
||||||
|
if let Some(avatar) = avatar {
|
||||||
|
tab.set_avatar_hash(avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
win.tabs_store().append(&tab);
|
||||||
|
}
|
||||||
|
xmpp_client::XMPPEvent::PM(jid, body, time) => {
|
||||||
|
let message = Message::new(&time, jid.as_str(), &body);
|
||||||
|
messages.insert_message(&jid, &message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut args = std::env::args();
|
env_logger::init();
|
||||||
let _ = args.next().unwrap();
|
|
||||||
let username = args.next().expect("Please give username argument 1");
|
let config = Arc::new(RwLock::new(ConfigFile::from_xdg()));
|
||||||
let password = args.next().expect("Please give password argument 2");
|
let messages = MessageStore::new();
|
||||||
|
let xmpp_cmd = xmpp_client::CommandSender::new();
|
||||||
|
|
||||||
let app = adw::Application::builder()
|
let app = adw::Application::builder()
|
||||||
.application_id("fr.linkmauve.XmppClient")
|
.application_id("fr.linkmauve.XmppClient")
|
||||||
.flags(gio::ApplicationFlags::HANDLES_COMMAND_LINE)
|
.flags(gio::ApplicationFlags::HANDLES_COMMAND_LINE)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let tabs_store = gio::ListStore::new::<Tab>();
|
app.connect_command_line(move |app, command_line| {
|
||||||
|
let args = command_line.arguments();
|
||||||
let (xmpp_receiver, cmd_sender) = xmpp_client::client(&username, &password);
|
let mut iter = args.iter();
|
||||||
let tabs_store_copy = tabs_store.clone();
|
iter.next().unwrap();
|
||||||
glib::spawn_future_local(async move {
|
let win = app.active_window().unwrap();
|
||||||
while let Ok(event) = xmpp_receiver.recv().await {
|
let win: &window::MainWindow = win.downcast_ref().unwrap();
|
||||||
match event {
|
for arg in iter {
|
||||||
xmpp::Event::ContactAdded(jid) => {
|
let arg = arg.to_str().unwrap().to_owned();
|
||||||
let tab = Tab::new(jid.jid.as_str(), jid.jid.as_str());
|
if let Some(jid) = arg.strip_prefix("xmpp:") {
|
||||||
tabs_store_copy.append(&tab);
|
let tab = Tab::new(jid, jid);
|
||||||
}
|
win.tabs_store().append(&tab);
|
||||||
xmpp::Event::ChatMessage(_id, _from, _body, _time) => {
|
|
||||||
// TODO: Insert message into tab history
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_ => continue,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
0
|
||||||
});
|
});
|
||||||
|
|
||||||
app.connect_startup(move |app| {
|
app.connect_startup(move |app| {
|
||||||
let win = window::MainWindow::new(app);
|
let win = window::MainWindow::new(app, config.clone());
|
||||||
|
|
||||||
let action_close = gio::ActionEntry::builder("close")
|
let action_close = gio::ActionEntry::builder("close")
|
||||||
.activate(|window: &window::MainWindow, _, _| {
|
.activate(|window: &window::MainWindow, _, _| {
|
||||||
|
@ -82,6 +141,19 @@ fn main() {
|
||||||
win.add_action_entries([action_close]);
|
win.add_action_entries([action_close]);
|
||||||
app.set_accels_for_action("win.close", &["<Ctrl>Q"]);
|
app.set_accels_for_action("win.close", &["<Ctrl>Q"]);
|
||||||
|
|
||||||
|
let win2 = win.clone();
|
||||||
|
let config2 = config.clone();
|
||||||
|
let messages2 = messages.clone();
|
||||||
|
let xmpp_cmd2 = xmpp_cmd.clone();
|
||||||
|
win.login().connect_clicked(move |_| {
|
||||||
|
on_login_pressed(
|
||||||
|
win2.clone(),
|
||||||
|
config2.clone(),
|
||||||
|
messages2.clone(),
|
||||||
|
xmpp_cmd2.clone(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
assert!(Tab::static_type().is_valid());
|
assert!(Tab::static_type().is_valid());
|
||||||
assert!(Message::static_type().is_valid());
|
assert!(Message::static_type().is_valid());
|
||||||
|
|
||||||
|
@ -125,24 +197,34 @@ 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: >k::ListItem = list_item.downcast_ref().unwrap();
|
let list_item: >k::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());
|
||||||
});
|
});
|
||||||
|
|
||||||
win.tabs().set_factory(Some(&tabs_factory));
|
win.tabs().set_factory(Some(&tabs_factory));
|
||||||
win.tabs_selection().set_model(Some(&tabs_store));
|
win.tabs_selection().set_model(Some(win.tabs_store()));
|
||||||
|
|
||||||
let win2 = win.clone();
|
let win2 = win.clone();
|
||||||
|
let messages2 = messages.clone();
|
||||||
win.tabs_selection()
|
win.tabs_selection()
|
||||||
.connect_selection_changed(move |tabs_selection, _, _| {
|
.connect_selection_changed(move |tabs_selection, _, _| {
|
||||||
let item = tabs_selection.selected_item().unwrap();
|
let item = tabs_selection.selected_item().unwrap();
|
||||||
let tab: &Tab = item.downcast_ref().unwrap();
|
let tab: &Tab = item.downcast_ref().unwrap();
|
||||||
println!("Switching to {}", tab.jid());
|
println!("Switching to {}", tab.jid());
|
||||||
let store = poezio_logs::load_logs(&tab.jid());
|
let store = messages2.with_jid(&xmpp::BareJid::new(&tab.jid()).unwrap());
|
||||||
let selection = win2.selection();
|
let selection = win2.selection();
|
||||||
selection.set_model(Some(&store));
|
selection.set_model(Some(&store));
|
||||||
win2.messages().scroll_to(
|
win2.messages().scroll_to(
|
||||||
|
@ -155,7 +237,7 @@ fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
let win2 = win.clone();
|
let win2 = win.clone();
|
||||||
let cmd_sender2 = cmd_sender.clone();
|
let xmpp_cmd2 = xmpp_cmd.clone();
|
||||||
win.entry().connect_activate(move |entry| {
|
win.entry().connect_activate(move |entry| {
|
||||||
let text = entry.text();
|
let text = entry.text();
|
||||||
entry.set_text("");
|
entry.set_text("");
|
||||||
|
@ -165,12 +247,10 @@ fn main() {
|
||||||
.selected_item()
|
.selected_item()
|
||||||
.and_downcast()
|
.and_downcast()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cmd_sender2
|
xmpp_cmd2.send(xmpp_client::XMPPCommand::SendPM(
|
||||||
.send_blocking(xmpp_client::XMPPCommand::SendPM(
|
xmpp::BareJid::new(¤t_tab.jid()).unwrap(),
|
||||||
xmpp::BareJid::new(¤t_tab.jid()).unwrap(),
|
text.as_str().to_string(),
|
||||||
text.as_str().to_string(),
|
));
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
let message = Message::now(&get_own_nick(), &text);
|
let message = Message::now(&get_own_nick(), &text);
|
||||||
let selection = win2.selection();
|
let selection = win2.selection();
|
||||||
let store = selection
|
let store = selection
|
||||||
|
|
|
@ -25,9 +25,9 @@ use nom::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::xdg::xdg_data;
|
||||||
use crate::Message;
|
use crate::Message;
|
||||||
|
|
||||||
pub trait LogItem {
|
pub trait LogItem {
|
||||||
|
@ -155,13 +155,7 @@ pub enum Item<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_logs(jid: &str) -> gio::ListStore {
|
pub fn load_logs(jid: &str) -> gio::ListStore {
|
||||||
let xdg_data = std::env::var("XDG_DATA_HOME").unwrap_or(format!(
|
let entry = xdg_data("poezio").join("logs").join(jid);
|
||||||
"{}/.local/share",
|
|
||||||
std::env::var("HOME").expect("NO $HOME?!")
|
|
||||||
));
|
|
||||||
let xdg_data = PathBuf::from(xdg_data);
|
|
||||||
let entry = xdg_data.join("poezio/logs").join(jid);
|
|
||||||
|
|
||||||
let logs = read_to_string(entry).unwrap_or("".to_string());
|
let logs = read_to_string(entry).unwrap_or("".to_string());
|
||||||
let (_, logs) = parse_logs(&logs).unwrap();
|
let (_, logs) = parse_logs(&logs).unwrap();
|
||||||
let store = gio::ListStore::new::<Message>();
|
let store = gio::ListStore::new::<Message>();
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,13 +68,14 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: currently file path, not a hash
|
||||||
|
pub fn set_avatar_hash(&self, hash: &str) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="name">
|
<object class="GtkLabel" id="name">
|
||||||
|
<property name="ellipsize">end</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">4</property>
|
<property name="margin-top">4</property>
|
||||||
<property name="margin-bottom">4</property>
|
<property name="margin-bottom">4</property>
|
||||||
|
|
|
@ -15,12 +15,30 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use gtk::glib;
|
use gtk::{gio, glib};
|
||||||
|
|
||||||
/// The private struct, which can hold widgets and other data.
|
/// The private struct, which can hold widgets and other data.
|
||||||
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
||||||
#[template(file = "window.ui")]
|
#[template(file = "window.ui")]
|
||||||
pub struct MainWindow {
|
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]
|
#[template_child]
|
||||||
pub split_view: TemplateChild<adw::NavigationSplitView>,
|
pub split_view: TemplateChild<adw::NavigationSplitView>,
|
||||||
#[template_child]
|
#[template_child]
|
||||||
|
|
|
@ -16,9 +16,14 @@
|
||||||
|
|
||||||
mod imp;
|
mod imp;
|
||||||
|
|
||||||
|
use adw::prelude::*;
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use gtk::{gio, glib};
|
use gtk::{gio, glib};
|
||||||
|
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use crate::config::ConfigFile;
|
||||||
|
|
||||||
glib::wrapper! {
|
glib::wrapper! {
|
||||||
pub struct MainWindow(ObjectSubclass<imp::MainWindow>)
|
pub struct MainWindow(ObjectSubclass<imp::MainWindow>)
|
||||||
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
|
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
|
||||||
|
@ -26,8 +31,37 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainWindow {
|
impl MainWindow {
|
||||||
pub fn new(app: &adw::Application) -> Self {
|
pub fn new(app: &adw::Application, config: Arc<RwLock<ConfigFile>>) -> Self {
|
||||||
glib::Object::builder().property("application", app).build()
|
let win: Self = glib::Object::builder().property("application", app).build();
|
||||||
|
if let Some((jid, password)) = config.read().unwrap().get_first_account() {
|
||||||
|
win.jid().set_text(jid.as_str());
|
||||||
|
win.password().set_text(password);
|
||||||
|
}
|
||||||
|
win
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
pub fn split_view(&self) -> &adw::NavigationSplitView {
|
||||||
|
|
|
@ -13,81 +13,147 @@
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<property name="content">
|
<property name="content">
|
||||||
<object class="AdwNavigationSplitView" id="split_view">
|
<object class="GtkStack" id="stack">
|
||||||
<property name="sidebar">
|
<child>
|
||||||
<object class="AdwNavigationPage">
|
<object class="GtkBox">
|
||||||
<property name="title" translatable="yes">Open Chats</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="child">
|
<child>
|
||||||
<object class="AdwToolbarView">
|
<object class="AdwHeaderBar"/>
|
||||||
<child type="top">
|
</child>
|
||||||
<object class="AdwHeaderBar"/>
|
<child>
|
||||||
</child>
|
<object class="AdwClamp">
|
||||||
<property name="content">
|
<child>
|
||||||
<object class="GtkScrolledWindow">
|
|
||||||
<property name="vexpand">yes</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkListView" id="tabs_list">
|
|
||||||
<property name="model">tabs_selection</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
<property name="content">
|
|
||||||
<object class="AdwNavigationPage">
|
|
||||||
<property name="title" translatable="yes">Messages</property>
|
|
||||||
<property name="tag">chat</property>
|
|
||||||
<property name="child">
|
|
||||||
<object class="AdwToolbarView">
|
|
||||||
<child type="top">
|
|
||||||
<object class="AdwHeaderBar"/>
|
|
||||||
</child>
|
|
||||||
<property name="content">
|
|
||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow">
|
<object class="AdwPreferencesGroup">
|
||||||
<property name="vexpand">yes</property>
|
<property name="title" translatable="yes">Enter your XMPP credentials</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkListView" id="message_list_view">
|
<object class="AdwEntryRow" id="jid">
|
||||||
<property name="model">selection</property>
|
<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>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkButton" id="login">
|
||||||
<property name="margin-start">10</property>
|
<property name="label" translatable="yes">Login</property>
|
||||||
<property name="margin-top">10</property>
|
<property name="css-classes">suggested-action</property>
|
||||||
<property name="margin-end">10</property>
|
<property name="margin-top">8</property>
|
||||||
<property name="margin-bottom">10</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkEntry" id="entry">
|
|
||||||
<property name="hexpand">yes</property>
|
|
||||||
<property name="placeholder-text" translatable="yes">Send a message</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<!--
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="send_input">
|
|
||||||
<property name="icon-name">go-next</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
-->
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</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">
|
||||||
|
<property name="title" translatable="yes">Open Chats</property>
|
||||||
|
<property name="child">
|
||||||
|
<object class="AdwToolbarView">
|
||||||
|
<child type="top">
|
||||||
|
<object class="AdwHeaderBar"/>
|
||||||
|
</child>
|
||||||
|
<property name="content">
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="vexpand">yes</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListView" id="tabs_list">
|
||||||
|
<property name="model">tabs_selection</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
<property name="content">
|
||||||
|
<object class="AdwNavigationPage">
|
||||||
|
<property name="title" translatable="yes">Messages</property>
|
||||||
|
<property name="tag">chat</property>
|
||||||
|
<property name="child">
|
||||||
|
<object class="AdwToolbarView">
|
||||||
|
<child type="top">
|
||||||
|
<object class="AdwHeaderBar"/>
|
||||||
|
</child>
|
||||||
|
<property name="content">
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="vexpand">yes</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListView" id="message_list_view">
|
||||||
|
<property name="model">selection</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="margin-start">10</property>
|
||||||
|
<property name="margin-top">10</property>
|
||||||
|
<property name="margin-end">10</property>
|
||||||
|
<property name="margin-bottom">10</property>
|
||||||
|
<child>
|
||||||
|
<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>
|
||||||
|
<!--
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="send_input">
|
||||||
|
<property name="icon-name">go-next</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
-->
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
</object>
|
||||||
</property>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
</template>
|
</template>
|
||||||
|
<object class="GListStore" id="tabs_store"/>
|
||||||
<object class="GtkSingleSelection" id="tabs_selection"/>
|
<object class="GtkSingleSelection" id="tabs_selection"/>
|
||||||
<object class="GtkNoSelection" id="selection"/>
|
<object class="GtkNoSelection" id="selection"/>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
26
src/xdg.rs
Normal file
26
src/xdg.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
|
||||||
|
pub fn xdg_data(app: &str) -> Utf8PathBuf {
|
||||||
|
let xdg_data = std::env::var("XDG_DATA_HOME").unwrap_or(format!(
|
||||||
|
"{}/.local/share",
|
||||||
|
std::env::var("HOME").expect("NO $HOME?!")
|
||||||
|
));
|
||||||
|
Utf8PathBuf::from(xdg_data).join(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn xdg_cache(app: &str) -> Utf8PathBuf {
|
||||||
|
let xdg_cache = std::env::var("XDG_CACHE_HOME").unwrap_or(format!(
|
||||||
|
"{}/.cache",
|
||||||
|
std::env::var("HOME").expect("NO $HOME?!")
|
||||||
|
));
|
||||||
|
Utf8PathBuf::from(xdg_cache).join(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xdg_config(app: &str) -> Utf8PathBuf {
|
||||||
|
let xdg_config = std::env::var("XDG_CONFIG_HOME").unwrap_or(format!(
|
||||||
|
"{}/.config",
|
||||||
|
std::env::var("HOME").expect("NO $HOME?!")
|
||||||
|
));
|
||||||
|
Utf8PathBuf::from(xdg_config).join(app)
|
||||||
|
}
|
|
@ -1,21 +1,117 @@
|
||||||
use async_channel::{Receiver, Sender};
|
use async_channel::{Receiver, Sender};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use xmpp::{BareJid, ClientBuilder, ClientFeature, Event};
|
use xmpp::{BareJid, ClientBuilder, ClientFeature};
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, OnceLock, RwLock};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum XMPPCommand {
|
pub enum XMPPCommand {
|
||||||
SendPM(BareJid, String),
|
SendPM(BareJid, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tokio_runtime() -> &'static Runtime {
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CommandSender(Arc<RwLock<Option<Sender<XMPPCommand>>>>);
|
||||||
|
|
||||||
|
impl CommandSender {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(Arc::new(RwLock::new(None)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sender(&self, sender: Sender<XMPPCommand>) {
|
||||||
|
*self.0.write().unwrap() = Some(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, command: XMPPCommand) {
|
||||||
|
self.0
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.expect("bug: CommandSender not init")
|
||||||
|
.send_blocking(command)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum XMPPEvent {
|
||||||
|
Online,
|
||||||
|
Avatar(BareJid, String),
|
||||||
|
Contact(BareJid, Option<String>),
|
||||||
|
PM(BareJid, String, DateTime<Utc>),
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if from.is_bare() {
|
||||||
|
Some(XMPPEvent::PM(
|
||||||
|
from.to_bare(),
|
||||||
|
body.0.to_string(),
|
||||||
|
time.received.clone(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
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 +120,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,22 +128,14 @@ 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::ContactAdded(_) => {
|
let _ = event_sender.send(parsed_event).await;
|
||||||
event_sender.send(event).await.expect("BOOOOOOHOOOO");
|
|
||||||
}
|
|
||||||
Event::ChatMessage(_, _, _, _) => {
|
|
||||||
event_sender.send(event).await.expect("BOOOHOOOO");
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
command = cmd_receiver.recv() => {
|
Ok(command) = cmd_receiver.recv() => {
|
||||||
match command.unwrap() {
|
match command {
|
||||||
XMPPCommand::SendPM(jid, content) => {
|
XMPPCommand::SendPM(jid, content) => {
|
||||||
client.send_message(jid.into(), xmpp::parsers::message::MessageType::Chat, "en", &content).await;
|
client.send_message(jid.into(), xmpp::parsers::message::MessageType::Chat, "en", &content).await;
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user