Browse Source

Listing room members

environments/review-jsparber-h-cxnwl8/deployments/1
Daniel García Moreno 9 years ago
parent
commit
1a94de3ef0
  1. 105
      res/main_window.glade
  2. 152
      src/app.rs
  3. 107
      src/backend.rs
  4. 1
      src/main.rs

105
res/main_window.glade

@ -2,6 +2,14 @@
<!-- Generated with glade 3.20.0 --> <!-- Generated with glade 3.20.0 -->
<interface> <interface>
<requires lib="gtk+" version="3.12"/> <requires lib="gtk+" version="3.12"/>
<object class="GtkListStore" id="members_store">
<columns>
<!-- column-name alias -->
<column type="gchararray"/>
<!-- column-name uid -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkTreeStore" id="rooms_tree_store"> <object class="GtkTreeStore" id="rooms_tree_store">
<columns> <columns>
<!-- column-name name --> <!-- column-name name -->
@ -185,46 +193,47 @@
<property name="left_attach">2</property> <property name="left_attach">2</property>
<property name="top_attach">0</property> <property name="top_attach">0</property>
</packing> </packing>
</child>
<child>
<object class="GtkScrolledWindow" id="messages_scroll">
<property name="width_request">200</property>
<property name="height_request">300</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="min_content_width">200</property>
<property name="min_content_height">300</property>
<property name="propagate_natural_width">True</property>
<child>
<object class="GtkListBox" id="message_list">
<property name="width_request">480</property>
<property name="height_request">360</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkStack" id="room_sidebar_stack"> <object class="GtkStack" id="room_sidebar_stack">
<property name="width_request">200</property> <property name="width_request">200</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<child> <child>
<object class="GtkLabel"> <object class="GtkScrolledWindow" id="member_scroll">
<property name="can_focus">False</property> <property name="visible">True</property>
<property name="label" translatable="yes">&lt;Members&gt;</property> <property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkTreeView" id="members_treeview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">members_store</property>
<property name="headers_visible">False</property>
<property name="enable_grid_lines">vertical</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="title" translatable="yes">name</property>
<child>
<object class="GtkCellRendererText">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object> </object>
<packing> <packing>
<property name="name">room_member_list</property> <property name="name">room_member_list</property>
@ -362,6 +371,34 @@
<property name="height">4</property> <property name="height">4</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkScrolledWindow" id="messages_scroll">
<property name="width_request">300</property>
<property name="height_request">300</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="message_list">
<property name="width_request">300</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="vexpand">True</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="name">room_page</property> <property name="name">room_page</property>

152
src/app.rs

@ -34,8 +34,8 @@ const APP_ID: &'static str = "org.gnome.guillotine";
struct AppOp { struct AppOp {
gtk_builder: gtk::Builder, gtk_builder: gtk::Builder,
backend: Backend, backend: Backend,
rooms: Option<HashMap<String, String>>,
active_room: String, active_room: String,
members: HashMap<String, backend::Member>,
} }
impl AppOp { impl AppOp {
@ -105,7 +105,7 @@ impl AppOp {
if let Ok(pixbuf) = Pixbuf::new_from_file_at_size(fname, 20, 20) { if let Ok(pixbuf) = Pixbuf::new_from_file_at_size(fname, 20, 20) {
image.set_from_pixbuf(&pixbuf); image.set_from_pixbuf(&pixbuf);
} else { } else {
image.set_from_stock("image-missing", 20); image.set_from_icon_name("image-missing", 2);
} }
self.show_username(); self.show_username();
@ -207,27 +207,38 @@ impl AppOp {
} }
pub fn set_rooms(&mut self, rooms: HashMap<String, String>) { pub fn set_rooms(&mut self, rooms: HashMap<String, String>) {
self.rooms = Some(rooms);
let store: gtk::TreeStore = self.gtk_builder.get_object("rooms_tree_store") let store: gtk::TreeStore = self.gtk_builder.get_object("rooms_tree_store")
.expect("Couldn't find rooms_tree_store in ui file."); .expect("Couldn't find rooms_tree_store in ui file.");
if let Some(ref rs) = self.rooms { for (id, name) in rooms {
for (id, name) in rs { let iter = store.insert_with_values(None, None,
let iter = store.insert_with_values(None, None, &[0, 1],
&[0, 1], &[&name, &id]);
&[&name, &id]);
}
} }
} }
pub fn set_active_room(&mut self, room: String) { pub fn set_active_room(&mut self, room: String) {
self.active_room = room; 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();
// getting room details // getting room details
self.backend.get_room_detail(self.active_room.clone(), String::from("m.room.name")).unwrap(); self.backend.get_room_detail(self.active_room.clone(), String::from("m.room.name")).unwrap();
self.backend.get_room_detail(self.active_room.clone(), String::from("m.room.topic")).unwrap(); 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_avatar(self.active_room.clone()).unwrap();
self.backend.get_room_messages(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) { pub fn set_room_detail(&self, key: String, value: String) {
@ -260,29 +271,101 @@ impl AppOp {
image.set_from_pixbuf(&pixbuf); image.set_from_pixbuf(&pixbuf);
} }
} else { } else {
image.set_from_stock("image-missing", 40); image.set_from_icon_name("image-missing", 5);
} }
} }
pub fn add_room_message(&self, msg: backend::Message) { pub fn scroll_down(&self) {
let s = self.gtk_builder let s = self.gtk_builder
.get_object::<gtk::ScrolledWindow>("messages_scroll") .get_object::<gtk::ScrolledWindow>("messages_scroll")
.expect("Can't find message_scroll in ui file."); .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());
}
}
pub fn add_room_message(&self, msg: backend::Message) {
let messages = self.gtk_builder let messages = self.gtk_builder
.get_object::<gtk::ListBox>("message_list") .get_object::<gtk::ListBox>("message_list")
.expect("Can't find message_list in ui file."); .expect("Can't find message_list in ui file.");
let body = msg.b; let body = msg.b;
let mut msg = gtk::Label::new(&body[..]); let sender = msg.s;
msg.set_line_wrap(true);
msg.set_justify(gtk::Justification::Left); let mut msg = gtk::Box::new(gtk::Orientation::Horizontal, 5);
msg.set_halign(gtk::Align::Start);
msg.show(); let mut vert = gtk::Box::new(gtk::Orientation::Vertical, 0);
let mut label = gtk::Label::new(&body[..]);
label.set_line_wrap(true);
label.set_justify(gtk::Justification::Left);
label.set_halign(gtk::Align::Start);
label.set_alignment(0 as f32, 0 as f32);
let mut fname = sender.clone();
let mut avatar_url = String::new();
if let Some(m) = self.members.get(&sender) {
fname = m.get_alias();
avatar_url = m.avatar.clone();
}
let avatar = gtk::Image::new_from_icon_name("image-missing", 5);
if !avatar_url.is_empty() {
let a = avatar.clone();
let fname = self.backend.get_media_async(avatar_url).unwrap();
let mut tries = 0;
gtk::timeout_add(50, move || {
match Pixbuf::new_from_file_at_size(&fname, 32, 32) {
Ok(pixbuf) => {
a.set_from_pixbuf(&pixbuf);
gtk::Continue(false)
},
Err(err) => {
match tries {
i if i < 200 => gtk::Continue(true),
_ => gtk::Continue(false),
}
}
}
});
}
let mut username = gtk::Label::new("");
username.set_markup(&format!("<span color=\"gray\">{}</span>", fname));
username.set_justify(gtk::Justification::Left);
username.set_halign(gtk::Align::Start);
vert.pack_start(&username, false, false, 0);
vert.pack_start(&label, true, true, 0);
msg.pack_start(&avatar, false, true, 5);
msg.pack_start(&vert, true, true, 0);
msg.show_all();
messages.add(&msg); messages.add(&msg);
}
if let Some(adj) = s.get_vadjustment() { pub fn add_room_member(&mut self, m: backend::Member) {
adj.set_value(adj.get_upper()); 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();
let iter = 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));
} }
} }
@ -313,13 +396,13 @@ impl App {
AppOp{ AppOp{
gtk_builder: gtk_builder.clone(), gtk_builder: gtk_builder.clone(),
backend: Backend::new(tx), backend: Backend::new(tx),
rooms: None,
active_room: String::from(""), active_room: String::from(""),
members: HashMap::new(),
} }
)); ));
let theop = op.clone(); let theop = op.clone();
gtk::timeout_add(50, move || { gtk::timeout_add(300, move || {
let recv = rx.try_recv(); let recv = rx.try_recv();
match recv { match recv {
Ok(backend::BKResponse::Token(uid, _)) => { Ok(backend::BKResponse::Token(uid, _)) => {
@ -348,6 +431,23 @@ impl App {
}, },
Ok(backend::BKResponse::RoomMessage(msg)) => { Ok(backend::BKResponse::RoomMessage(msg)) => {
theop.lock().unwrap().add_room_message(msg); theop.lock().unwrap().add_room_message(msg);
theop.lock().unwrap().scroll_down();
},
Ok(backend::BKResponse::RoomMessages(msgs)) => {
for msg in msgs {
theop.lock().unwrap().add_room_message(msg);
}
theop.lock().unwrap().scroll_down();
},
Ok(backend::BKResponse::RoomMember(member)) => {
theop.lock().unwrap().add_room_member(member);
},
Ok(backend::BKResponse::RoomMembers(members)) => {
for m in members {
theop.lock().unwrap().add_room_member(m);
}
},
Ok(backend::BKResponse::RoomMemberAvatar(uid, avatar)) => {
}, },
Err(_) => { }, Err(_) => { },
}; };
@ -403,6 +503,18 @@ impl App {
op_c.lock().unwrap().set_active_room(id.get().unwrap()); op_c.lock().unwrap().set_active_room(id.get().unwrap());
}); });
// member selection
let members: gtk::TreeView = gtk_builder.get_object("members_treeview")
.expect("Couldn't find members_treeview in ui file.");
op_c = op.clone();
members.set_activate_on_single_click(true);
members.connect_row_activated(move |view, path, column| {
let iter = view.get_model().unwrap().get_iter(path).unwrap();
let id = view.get_model().unwrap().get_value(&iter, 1);
op_c.lock().unwrap().member_clicked(id.get().unwrap());
});
// Login click // Login click
let login_btn: gtk::Button = gtk_builder.get_object("login_button") let login_btn: gtk::Button = gtk_builder.get_object("login_button")
.expect("Couldn't find login_button in ui file."); .expect("Couldn't find login_button in ui file.");

107
src/backend.rs

@ -134,6 +134,10 @@ pub enum BKResponse {
RoomDetail(String, String), RoomDetail(String, String),
RoomAvatar(String), RoomAvatar(String),
RoomMessage(Message), RoomMessage(Message),
RoomMessages(Vec<Message>),
RoomMember(Member),
RoomMembers(Vec<Member>),
RoomMemberAvatar(String, String),
} }
#[derive(Debug)] #[derive(Debug)]
@ -148,6 +152,22 @@ pub struct Message {
pub a: i64, pub a: i64,
} }
#[derive(Debug)]
pub struct Member {
pub alias: String,
pub uid: String,
pub avatar: String,
}
impl Member {
pub fn get_alias(&self) -> String {
match self.alias {
ref a if a.is_empty() => self.uid.clone(),
ref a => a.clone(),
}
}
}
impl Backend { impl Backend {
pub fn new(tx: Sender<BKResponse>) -> Backend { pub fn new(tx: Sender<BKResponse>) -> Backend {
@ -352,20 +372,101 @@ impl Backend {
let tx = self.tx.clone(); let tx = self.tx.clone();
get!(url, map, get!(url, map,
|r: JsonValue| { |r: JsonValue| {
let mut ms: Vec<Message> = vec![];
for msg in r["chunk"].as_array().unwrap().iter().rev() { for msg in r["chunk"].as_array().unwrap().iter().rev() {
println!("messages: {:#?}", msg); //println!("messages: {:#?}", msg);
let m = Message { let m = Message {
s: String::from(msg["sender"].as_str().unwrap()), s: String::from(msg["sender"].as_str().unwrap()),
t: String::from(msg["content"]["msgtype"].as_str().unwrap()), t: String::from(msg["content"]["msgtype"].as_str().unwrap()),
b: String::from(msg["content"]["body"].as_str().unwrap()), b: String::from(msg["content"]["body"].as_str().unwrap()),
a: msg["age"].as_i64().unwrap(), a: msg["age"].as_i64().unwrap(),
}; };
tx.send(BKResponse::RoomMessage(m)).unwrap(); ms.push(m);
}
tx.send(BKResponse::RoomMessages(ms)).unwrap();
});
Ok(())
}
pub fn get_room_members(&self, roomid: String) -> Result<(), Error> {
let s = self.data.lock().unwrap().server_url.clone();
let tk = self.data.lock().unwrap().access_token.clone();
let baseu = Url::parse(&s)?;
let mut url = baseu.join("/_matrix/client/r0/rooms/")?.join(&(roomid + "/"))?.join("members")?;
url = url.join(&format!("?access_token={}", tk))?;
let map: HashMap<String, String> = HashMap::new();
let tx = self.tx.clone();
get!(url, map,
|r: JsonValue| {
//println!("{:#?}", r);
let mut ms: Vec<Member> = vec![];
for member in r["chunk"].as_array().unwrap().iter().rev() {
if member["type"].as_str().unwrap() != "m.room.member" {
continue;
}
let content = &member["content"];
if content["membership"].as_str().unwrap() != "join" {
continue;
}
let m = Member {
alias: String::from(content["displayname"].as_str().unwrap_or("")),
uid: String::from(member["sender"].as_str().unwrap()),
avatar: String::from(content["avatar_url"].as_str().unwrap_or("")),
};
ms.push(m);
if (ms.len() > 20) {
tx.send(BKResponse::RoomMembers(ms)).unwrap();
ms = vec![];
}
} }
if (!ms.is_empty()) {
tx.send(BKResponse::RoomMembers(ms)).unwrap();
}
});
Ok(())
}
pub fn get_member_avatar(&self, memberid: String, avatar_url: String) -> Result<(), Error> {
let s = self.data.lock().unwrap().server_url.clone();
let baseu = Url::parse(&s)?;
let tx = self.tx.clone();
thread::spawn(move || {
let fname = thumb!(baseu, &avatar_url).unwrap();
tx.send(BKResponse::RoomMemberAvatar(memberid, fname)).unwrap();
}); });
Ok(()) Ok(())
} }
pub fn get_base_url(&self) -> Result<Url, Error> {
let s = self.data.lock().unwrap().server_url.clone();
let url = Url::parse(&s)?;
Ok(url)
}
pub fn get_media_async(&self, url: String) -> Result<String, Error> {
let base = self.get_base_url()?;
let xdg_dirs = xdg::BaseDirectories::with_prefix("guillotine").unwrap();
let re = Regex::new(r"mxc://(?P<server>[^/]+)/(?P<media>.+)")?;
let caps = re.captures(&url).ok_or(Error::BackendError)?;
let media = String::from(&caps["media"]);
let fname = String::from(xdg_dirs.place_cache_file(&media)?.to_str().ok_or(Error::BackendError)?);
let u = url.clone();
thread::spawn(move || {
thumb!(base, &u).unwrap();
});
Ok(fname)
}
} }
fn get_rooms_from_json(r: JsonValue) -> Result<HashMap<String, String>, Error> { fn get_rooms_from_json(r: JsonValue) -> Result<HashMap<String, String>, Error> {
@ -403,6 +504,8 @@ fn get_media(url: &str) -> Result<Vec<u8>, Error> {
} }
fn dw_media(base: Url, url: &str, thumb: bool, dest: Option<&str>, w: i32, h: i32) -> Result<String, Error> { fn dw_media(base: Url, url: &str, thumb: bool, dest: Option<&str>, w: i32, h: i32) -> Result<String, Error> {
// TODO, don't download if exists
let xdg_dirs = xdg::BaseDirectories::with_prefix("guillotine").unwrap(); let xdg_dirs = xdg::BaseDirectories::with_prefix("guillotine").unwrap();
let re = Regex::new(r"mxc://(?P<server>[^/]+)/(?P<media>.+)")?; let re = Regex::new(r"mxc://(?P<server>[^/]+)/(?P<media>.+)")?;

1
src/main.rs

@ -1,6 +1,5 @@
#[macro_use] #[macro_use]
mod util; mod util;
mod backend; mod backend;
mod app; mod app;

Loading…
Cancel
Save