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.
 
 
 

620 lines
20 KiB

extern crate gtk;
extern crate gio;
extern crate gdk_pixbuf;
extern crate chrono;
extern crate secret_service;
use self::secret_service::SecretService;
use self::secret_service::EncryptionType;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::channel;
use std::sync::mpsc::{Sender, Receiver};
use std::collections::HashMap;
use self::gio::ApplicationExt;
use self::gdk_pixbuf::Pixbuf;
use self::gtk::prelude::*;
use self::chrono::prelude::*;
use backend::Backend;
use backend;
#[derive(Debug)]
pub enum Error {
SecretServiceError,
}
derror!(secret_service::SsError, Error::SecretServiceError);
// TODO: Is this the correct format for GApplication IDs?
const APP_ID: &'static str = "org.gnome.guillotine";
struct AppOp {
gtk_builder: gtk::Builder,
backend: Backend,
active_room: String,
members: HashMap<String, backend::Member>,
}
impl AppOp {
pub fn login(&self) {
let user_entry: gtk::Entry = self.gtk_builder.get_object("login_username")
.expect("Can't find login_username in ui file.");
let pass_entry: gtk::Entry = self.gtk_builder.get_object("login_password")
.expect("Can't find login_password in ui file.");
let server_entry: gtk::Entry = self.gtk_builder.get_object("login_server")
.expect("Can't find login_server in ui file.");
let username = match user_entry.get_text() { Some(s) => s, None => String::from("") };
let password = match pass_entry.get_text() { Some(s) => s, None => String::from("") };
self.connect(username, password, server_entry.get_text());
}
pub fn connect(&self, username: String, password: String, server: Option<String>) {
let server_url = match server {
Some(s) => s,
None => String::from("https://matrix.org")
};
self.store_pass(username.clone(), password.clone(), server_url.clone())
.unwrap_or_else(|_| {
// TODO: show an error
println!("Error: Can't store the password using libsecret");
});
self.show_loading();
self.backend.login(username.clone(), password.clone(), server_url.clone())
.unwrap_or_else(move |_| {
// TODO: show an error
println!("Error: Can't login with {} in {}", username, server_url);
});
self.hide_popup();
}
pub fn connect_guest(&self, server: Option<String>) {
let server_url = match server {
Some(s) => s,
None => String::from("https://matrix.org")
};
self.show_loading();
self.backend.guest(server_url).unwrap();
}
pub fn get_username(&self) {
self.backend.get_username().unwrap();
self.backend.get_avatar().unwrap();
}
pub fn set_username(&self, username: &str) {
self.gtk_builder
.get_object::<gtk::Label>("display_name_label")
.expect("Can't find display_name_label in ui file.")
.set_text(username);
self.show_username();
}
pub fn set_avatar(&self, fname: &str) {
let image = self.gtk_builder
.get_object::<gtk::Image>("profile_image")
.expect("Can't find profile_image in ui file.");
if let Ok(pixbuf) = Pixbuf::new_from_file_at_size(fname, 20, 20) {
image.set_from_pixbuf(&pixbuf);
} else {
image.set_from_icon_name("image-missing", 2);
}
self.show_username();
}
pub fn show_username(&self) {
self.gtk_builder
.get_object::<gtk::Stack>("user_button_stack")
.expect("Can't find user_button_stack in ui file.")
.set_visible_child_name("user_connected_page");
}
pub fn show_loading(&self) {
self.gtk_builder
.get_object::<gtk::Stack>("user_button_stack")
.expect("Can't find user_button_stack in ui file.")
.set_visible_child_name("user_loading_page");
}
pub fn hide_popup(&self) {
let user_menu: gtk::Popover = self.gtk_builder.get_object("user_menu")
.expect("Couldn't find user_menu in ui file.");
user_menu.hide();
}
pub fn disconnect(&self) {
println!("Disconnecting");
}
pub fn store_pass(&self, username: String, password: String, server: String) -> Result<(), Error> {
let ss = SecretService::new(EncryptionType::Dh)?;
let collection = ss.get_default_collection()?;
// deleting previous items
let allpass = collection.get_all_items()?;
let passwds = allpass.iter()
.filter(|x| x.get_label().unwrap_or(String::from("")) == "guillotine");
for p in passwds {
p.delete()?;
}
// create new item
collection.create_item(
"guillotine", // label
vec![
("username", &username),
("server", &server),
], // properties
password.as_bytes(), //secret
true, // replace item with same attributes
"text/plain" // secret content type
)?;
Ok(())
}
pub fn get_pass(&self) -> Result<(String, String, String), Error> {
let ss = SecretService::new(EncryptionType::Dh)?;
let collection = ss.get_default_collection()?;
let allpass = collection.get_all_items()?;
let passwd = allpass.iter()
.find(|x| x.get_label().unwrap_or(String::from("")) == "guillotine");
if passwd.is_none() {
return Err(Error::SecretServiceError);
}
let p = passwd.unwrap();
let attrs = p.get_attributes()?;
let secret = p.get_secret()?;
let mut attr = attrs.iter().find(|&ref x| x.0 == "username")
.ok_or(Error::SecretServiceError)?;
let username = attr.1.clone();
attr = attrs.iter().find(|&ref x| x.0 == "server")
.ok_or(Error::SecretServiceError)?;
let server = attr.1.clone();
let tup = (
username,
String::from_utf8(secret).unwrap(),
server,
);
Ok(tup)
}
pub fn init(&self) {
if let Ok(pass) = self.get_pass() {
self.connect(pass.0, pass.1, Some(pass.2));
} else {
self.connect_guest(None);
}
}
pub fn sync(&self) {
self.backend.sync().unwrap();
}
pub fn set_rooms(&mut self, rooms: HashMap<String, String>) {
let store: gtk::TreeStore = self.gtk_builder.get_object("rooms_tree_store")
.expect("Couldn't find rooms_tree_store in ui file.");
let mut array: Vec<(String, String)> = vec![];
for (id, name) in rooms {
array.push((name, id));
}
array.sort_by(|x, y| x.0.to_lowercase().cmp(&y.0.to_lowercase()));
for v in array {
store.insert_with_values(None, None,
&[0, 1],
&[&v.0, &v.1]);
}
}
pub fn set_active_room(&mut self, room: String, name: String) {
self.active_room = room;
let messages = self.gtk_builder
.get_object::<gtk::ListBox>("message_list")
.expect("Can't find message_list in ui file.");
for ch in messages.get_children() {
messages.remove(&ch);
}
self.members.clear();
let members = self.gtk_builder
.get_object::<gtk::ListStore>("members_store")
.expect("Can't find members_store in ui file.");
members.clear();
let name_label = self.gtk_builder
.get_object::<gtk::Label>("room_name")
.expect("Can't find room_name in ui file.");
name_label.set_text(&name);
// getting room details
self.backend.get_room_detail(self.active_room.clone(), String::from("m.room.topic")).unwrap();
self.backend.get_room_avatar(self.active_room.clone()).unwrap();
self.backend.get_room_messages(self.active_room.clone()).unwrap();
self.backend.get_room_members(self.active_room.clone()).unwrap();
}
pub fn set_room_detail(&self, key: String, value: String) {
let k: &str = &key;
match k {
"m.room.name" => {
let name_label = self.gtk_builder
.get_object::<gtk::Label>("room_name")
.expect("Can't find room_name in ui file.");
name_label.set_text(&value);
},
"m.room.topic" => {
let topic_label = self.gtk_builder
.get_object::<gtk::Label>("room_topic")
.expect("Can't find room_topic in ui file.");
topic_label.set_tooltip_text(&value[..]);
topic_label.set_text(&value);
}
_ => { println!("no key {}", key) }
};
}
pub fn set_room_avatar(&self, avatar: String) {
let image = self.gtk_builder
.get_object::<gtk::Image>("room_image")
.expect("Can't find room_image in ui file.");
if !avatar.is_empty() {
if let Ok(pixbuf) = Pixbuf::new_from_file_at_size(&avatar, 40, 40) {
image.set_from_pixbuf(&pixbuf);
}
} else {
image.set_from_icon_name("image-missing", 5);
}
}
pub fn scroll_down(&self) {
let s = self.gtk_builder
.get_object::<gtk::ScrolledWindow>("messages_scroll")
.expect("Can't find message_scroll in ui file.");
if let Some(adj) = s.get_vadjustment() {
println!("adj: {:?}", adj);
adj.set_value(adj.get_upper());
}
}
fn build_room_msg_avatar(&self, sender: &str) -> gtk::Image {
let avatar = gtk::Image::new_from_icon_name("image-missing", 5);
let a = avatar.clone();
let (tx, rx): (Sender<String>, Receiver<String>) = channel();
self.backend.get_avatar_async(sender, tx).unwrap();
gtk::timeout_add(50, move || {
match rx.try_recv() {
Err(_) => gtk::Continue(true),
Ok(fname) => {
if let Ok(pixbuf) = Pixbuf::new_from_file_at_scale(&fname, 32, 32, false) {
a.set_from_pixbuf(&pixbuf);
}
gtk::Continue(false)
}
}
});
avatar.set_alignment(0.5, 0.);
avatar
}
fn build_room_msg_username(&self, sender: &str) -> gtk::Label {
let uname = match self.members.get(sender) {
Some(m) => m.get_alias(),
None => String::from(sender)
};
let username = gtk::Label::new("");
username.set_markup(&format!("<b>{}</b>", uname));
username.set_justify(gtk::Justification::Left);
username.set_halign(gtk::Align::Start);
username
}
fn build_room_msg_body(&self, body: &str) -> gtk::Label {
let msg = gtk::Label::new(body);
msg.set_line_wrap(true);
msg.set_justify(gtk::Justification::Left);
msg.set_halign(gtk::Align::Start);
msg.set_alignment(0 as f32, 0 as f32);
msg
}
fn build_room_msg_date(&self, dt: &DateTime<Local>) -> gtk::Label {
let d = dt.format("%d/%b/%y %H:%M").to_string();
let date = gtk::Label::new("");
date.set_markup(&format!("<span alpha=\"60%\">{}</span>", d));
date.set_line_wrap(true);
date.set_justify(gtk::Justification::Right);
date.set_halign(gtk::Align::End);
date.set_alignment(1 as f32, 0 as f32);
date
}
fn build_room_msg_info(&self, msg: &backend::Message) -> gtk::Box {
// info
// +----------+------+
// | username | date |
// +----------+------+
let info = gtk::Box::new(gtk::Orientation::Horizontal, 0);
let username = self.build_room_msg_username(&msg.sender);
let date = self.build_room_msg_date(&msg.date);
info.pack_start(&username, true, true, 0);
info.pack_start(&date, false, false, 0);
info
}
fn build_room_msg_content(&self, msg: &backend::Message) -> gtk::Box {
// content
// +------+
// | info |
// +------+
// | body |
// +------+
let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
let info = self.build_room_msg_info(msg);
let body = self.build_room_msg_body(&msg.body);
content.pack_start(&info, false, false, 0);
content.pack_start(&body, true, true, 0);
content
}
fn build_room_msg(&self, msg: backend::Message) -> gtk::Box {
let avatar = self.build_room_msg_avatar(&msg.sender);
// msg
// +--------+---------+
// | avatar | content |
// +--------+---------+
let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 5);
let content = self.build_room_msg_content(&msg);
msg_widget.pack_start(&avatar, false, false, 5);
msg_widget.pack_start(&content, true, true, 0);
msg_widget.show_all();
msg_widget
}
pub fn add_room_message(&self, msg: backend::Message) {
let messages = self.gtk_builder
.get_object::<gtk::ListBox>("message_list")
.expect("Can't find message_list in ui file.");
let msg = self.build_room_msg(msg);
messages.add(&msg);
}
pub fn add_room_member(&mut self, m: backend::Member) {
if !m.avatar.is_empty() {
self.backend.get_member_avatar(m.uid.clone(), m.avatar.clone()).unwrap();
}
let store: gtk::ListStore = self.gtk_builder.get_object("members_store")
.expect("Couldn't find members_store in ui file.");
let name = m.get_alias();
store.insert_with_values(None,
&[0, 1],
&[&name, &(m.uid)]);
self.members.insert(m.uid.clone(), m);
}
pub fn member_clicked(&self, uid: String) {
println!("member clicked: {}, {:?}", uid, self.members.get(&uid));
}
}
/// State for the main thread.
///
/// It takes care of starting up the application and for loading and accessing the
/// UI.
pub struct App {
/// GTK Application which runs the main loop.
gtk_app: gtk::Application,
/// Used to access the UI elements.
gtk_builder: gtk::Builder,
op: Arc<Mutex<AppOp>>,
}
impl App {
/// Create an App instance
pub fn new() -> App {
let gtk_app = gtk::Application::new(Some(APP_ID), gio::ApplicationFlags::empty())
.expect("Failed to initialize GtkApplication");
let (tx, rx): (Sender<backend::BKResponse>, Receiver<backend::BKResponse>) = channel();
let gtk_builder = gtk::Builder::new_from_file("res/main_window.glade");
let op = Arc::new(Mutex::new(
AppOp{
gtk_builder: gtk_builder.clone(),
backend: Backend::new(tx),
active_room: String::from(""),
members: HashMap::new(),
}
));
let theop = op.clone();
gtk::timeout_add(300, move || {
let recv = rx.try_recv();
match recv {
Ok(backend::BKResponse::Token(uid, _)) => {
theop.lock().unwrap().set_username(&uid);
theop.lock().unwrap().get_username();
theop.lock().unwrap().sync();
},
Ok(backend::BKResponse::Name(username)) => {
theop.lock().unwrap().set_username(&username);
},
Ok(backend::BKResponse::Avatar(path)) => {
theop.lock().unwrap().set_avatar(&path);
},
Ok(backend::BKResponse::Sync) => {
println!("SYNC");
theop.lock().unwrap().sync();
},
Ok(backend::BKResponse::Rooms(rooms)) => {
theop.lock().unwrap().set_rooms(rooms);
},
Ok(backend::BKResponse::RoomDetail(key, value)) => {
theop.lock().unwrap().set_room_detail(key, value);
},
Ok(backend::BKResponse::RoomAvatar(avatar)) => {
theop.lock().unwrap().set_room_avatar(avatar);
},
Ok(backend::BKResponse::RoomMessages(msgs)) => {
for msg in msgs {
theop.lock().unwrap().add_room_message(msg);
}
theop.lock().unwrap().scroll_down();
},
Ok(backend::BKResponse::RoomMembers(members)) => {
for m in members {
theop.lock().unwrap().add_room_member(m);
}
},
Ok(backend::BKResponse::RoomMemberAvatar(_, _)) => {
},
// errors
Ok(err) => {
println!("Query error: {:?}", err);
}
Err(_) => { },
};
gtk::Continue(true)
});
let app = App {
gtk_app,
gtk_builder,
op: op.clone(),
};
app.connect_gtk();
app
}
pub fn connect_gtk(&self) {
// Set up shutdown callback
let window: gtk::Window = self.gtk_builder.get_object("main_window")
.expect("Couldn't find main_window in ui file.");
window.set_title("Guillotine");
window.show_all();
let op = self.op.clone();
window.connect_delete_event(move |_, _| {
op.lock().unwrap().disconnect();
gtk::main_quit();
Inhibit(false)
});
self.gtk_app.connect_startup(move |app| {
window.set_application(app);
});
self.connect_user_button();
self.connect_login_button();
self.connect_room_treeview();
self.connect_member_treeview();
}
fn connect_user_button(&self) {
// Set up user popover
let user_button: gtk::Button = self.gtk_builder.get_object("user_button")
.expect("Couldn't find user_button in ui file.");
let user_menu: gtk::Popover = self.gtk_builder.get_object("user_menu")
.expect("Couldn't find user_menu in ui file.");
user_button.connect_clicked(move |_| user_menu.show_all());
}
fn connect_login_button(&self) {
// Login click
let login_btn: gtk::Button = self.gtk_builder.get_object("login_button")
.expect("Couldn't find login_button in ui file.");
let op = self.op.clone();
login_btn.connect_clicked(move |_| op.lock().unwrap().login());
}
fn connect_room_treeview(&self) {
// room selection
let treeview: gtk::TreeView = self.gtk_builder.get_object("rooms_tree_view")
.expect("Couldn't find rooms_tree_view in ui file.");
let op = self.op.clone();
treeview.set_activate_on_single_click(true);
treeview.connect_row_activated(move |view, path, _| {
let iter = view.get_model().unwrap().get_iter(path).unwrap();
let id = view.get_model().unwrap().get_value(&iter, 1);
let name = view.get_model().unwrap().get_value(&iter, 0);
op.lock().unwrap().set_active_room(id.get().unwrap(), name.get().unwrap());
});
}
fn connect_member_treeview(&self) {
// member selection
let members: gtk::TreeView = self.gtk_builder.get_object("members_treeview")
.expect("Couldn't find members_treeview in ui file.");
let op = self.op.clone();
members.set_activate_on_single_click(true);
members.connect_row_activated(move |view, path, _| {
let iter = view.get_model().unwrap().get_iter(path).unwrap();
let id = view.get_model().unwrap().get_value(&iter, 1);
op.lock().unwrap().member_clicked(id.get().unwrap());
});
}
pub fn run(self) {
self.op.lock().unwrap().init();
gtk::main();
}
}