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 -->
<interface>
<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">
<columns>
<!-- column-name name -->
@ -185,46 +193,47 @@
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</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>
<object class="GtkStack" id="room_sidebar_stack">
<property name="width_request">200</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel">
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;Members&gt;</property>
<object class="GtkScrolledWindow" id="member_scroll">
<property name="visible">True</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>
<packing>
<property name="name">room_member_list</property>
@ -362,6 +371,34 @@
<property name="height">4</property>
</packing>
</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>
<packing>
<property name="name">room_page</property>

152
src/app.rs

@ -34,8 +34,8 @@ const APP_ID: &'static str = "org.gnome.guillotine";
struct AppOp {
gtk_builder: gtk::Builder,
backend: Backend,
rooms: Option<HashMap<String, String>>,
active_room: String,
members: HashMap<String, backend::Member>,
}
impl AppOp {
@ -105,7 +105,7 @@ impl AppOp {
if let Ok(pixbuf) = Pixbuf::new_from_file_at_size(fname, 20, 20) {
image.set_from_pixbuf(&pixbuf);
} else {
image.set_from_stock("image-missing", 20);
image.set_from_icon_name("image-missing", 2);
}
self.show_username();
@ -207,27 +207,38 @@ impl AppOp {
}
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")
.expect("Couldn't find rooms_tree_store in ui file.");
if let Some(ref rs) = self.rooms {
for (id, name) in rs {
let iter = store.insert_with_values(None, None,
&[0, 1],
&[&name, &id]);
}
for (id, name) in rooms {
let iter = store.insert_with_values(None, None,
&[0, 1],
&[&name, &id]);
}
}
pub fn set_active_room(&mut self, room: 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();
// 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.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) {
@ -260,29 +271,101 @@ impl AppOp {
image.set_from_pixbuf(&pixbuf);
}
} 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
.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());
}
}
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 body = msg.b;
let mut msg = gtk::Label::new(&body[..]);
msg.set_line_wrap(true);
msg.set_justify(gtk::Justification::Left);
msg.set_halign(gtk::Align::Start);
msg.show();
let sender = msg.s;
let mut msg = gtk::Box::new(gtk::Orientation::Horizontal, 5);
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);
}
if let Some(adj) = s.get_vadjustment() {
adj.set_value(adj.get_upper());
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();
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{
gtk_builder: gtk_builder.clone(),
backend: Backend::new(tx),
rooms: None,
active_room: String::from(""),
members: HashMap::new(),
}
));
let theop = op.clone();
gtk::timeout_add(50, move || {
gtk::timeout_add(300, move || {
let recv = rx.try_recv();
match recv {
Ok(backend::BKResponse::Token(uid, _)) => {
@ -348,6 +431,23 @@ impl App {
},
Ok(backend::BKResponse::RoomMessage(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(_) => { },
};
@ -403,6 +503,18 @@ impl App {
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
let login_btn: gtk::Button = gtk_builder.get_object("login_button")
.expect("Couldn't find login_button in ui file.");

107
src/backend.rs

@ -134,6 +134,10 @@ pub enum BKResponse {
RoomDetail(String, String),
RoomAvatar(String),
RoomMessage(Message),
RoomMessages(Vec<Message>),
RoomMember(Member),
RoomMembers(Vec<Member>),
RoomMemberAvatar(String, String),
}
#[derive(Debug)]
@ -148,6 +152,22 @@ pub struct Message {
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 {
pub fn new(tx: Sender<BKResponse>) -> Backend {
@ -352,20 +372,101 @@ impl Backend {
let tx = self.tx.clone();
get!(url, map,
|r: JsonValue| {
let mut ms: Vec<Message> = vec![];
for msg in r["chunk"].as_array().unwrap().iter().rev() {
println!("messages: {:#?}", msg);
//println!("messages: {:#?}", msg);
let m = Message {
s: String::from(msg["sender"].as_str().unwrap()),
t: String::from(msg["content"]["msgtype"].as_str().unwrap()),
b: String::from(msg["content"]["body"].as_str().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(())
}
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> {
@ -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> {
// TODO, don't download if exists
let xdg_dirs = xdg::BaseDirectories::with_prefix("guillotine").unwrap();
let re = Regex::new(r"mxc://(?P<server>[^/]+)/(?P<media>.+)")?;

1
src/main.rs

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

Loading…
Cancel
Save