From d5adf274c137bef22d5695d4a280e10068edcb0e Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Wed, 23 Jun 2010 20:37:31 +0200 Subject: Show avatars in roster --- data/ephraim.glade | 14 ++++++++--- src/core/CMakeLists.txt | 1 + src/core/ephraim.erl | 38 ++++++++++++++++++++--------- src/core/ephraim_conn.erl | 7 +++++- src/core/ephraim_conv.erl | 4 ++-- src/core/ephraim_event.erl | 42 ++++++++++++++++++++++++++++++++ src/core/ephraim_roster.erl | 52 ++++++++++++++++++++++++++++++++-------- src/gui/CMakeLists.txt | 1 + src/gui/CellRendererAvatar.vala | 19 +++++++++++++++ src/gui/CellRendererContact.vala | 17 ++++++++++++- src/gui/Contact.vala | 2 ++ src/gui/CoreConnector.vala | 17 ++++++++++++- src/gui/Ephraim.vala | 24 +++++++++++++++---- src/gui/Roster.vala | 4 +++- 14 files changed, 207 insertions(+), 35 deletions(-) create mode 100644 src/core/ephraim_event.erl create mode 100644 src/gui/CellRendererAvatar.vala diff --git a/data/ephraim.glade b/data/ephraim.glade index 629e2de..c4f2740 100644 --- a/data/ephraim.glade +++ b/data/ephraim.glade @@ -72,12 +72,20 @@ True True - 160 + 200 - + True True - False + never + automatic + + + True + True + False + + False diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 792f279..c49763c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -2,6 +2,7 @@ erl_target(ephraim-core ephraim.erl ephraim_conn.erl ephraim_conv.erl + ephraim_event.erl ephraim_roster.erl ephraim_util.erl OPTIONS diff --git a/src/core/ephraim.erl b/src/core/ephraim.erl index 523749c..bd36118 100644 --- a/src/core/ephraim.erl +++ b/src/core/ephraim.erl @@ -4,6 +4,7 @@ -record(state, { conn :: pid(), roster :: pid(), + event :: pid(), convs :: dict(), uis :: set() }). @@ -25,29 +26,35 @@ init() -> Conn = spawn(ephraim_conn, init, []), Roster = spawn(ephraim_roster, init, []), - loop(#state{conn=Conn,roster=Roster,convs=dict:new(),uis=sets:new()}), + Event = spawn(ephraim_event, init, []), + loop(#state{conn=Conn,roster=Roster,event=Event,convs=dict:new(),uis=sets:new()}), init:stop(). --spec get_conv(#state{},exmpp_jid:jid()) -> {#state{},pid()}. +-spec get_conv(#state{},exmpp_jid:jid()) -> {#state{},pid()|undefined}. get_conv(State, JID) -> Key = exmpp_jid:to_lower(JID), - case dict:find(Key, State#state.convs) of - {ok, Conv} -> - {State, Conv}; - error -> - Conv = spawn(ephraim_conv, init, [Key]), - Dict = dict:store(Key, Conv, State#state.convs), - {State#state{convs=Dict}, Conv} + case Key of + {_,_,undefined} -> + {State, undefined}; + _ -> + case dict:find(Key, State#state.convs) of + {ok, Conv} -> + {State, Conv}; + error -> + Conv = spawn(ephraim_conv, init, [Key]), + Dict = dict:store(Key, Conv, State#state.convs), + {State#state{convs=Dict}, Conv} + end end. -spec loop(#state{}) -> ok. loop(State) -> receive stop -> - %dict:fold(fun(_,Conv,Msg) -> Conv ! Msg end, stop, State#state.convs), ephraim_util:send_all_values(State#state.convs, stop), State#state.conn ! stop, State#state.roster ! stop, + State#state.event ! stop, ok; {register_ui, Pid} -> @@ -78,9 +85,18 @@ loop(State) -> {receive_message, From, Packet} -> {State2, Conv} = get_conv(State, From), - Conv ! {receive_message, Packet}, + case Conv of + undefined -> + io:format("~p~n", [Packet]); + _ -> + Conv ! {receive_message, Packet} + end, loop(State2); + {receive_event, From, Packet} -> + State#state.event ! {receive_event, From, Packet}, + loop(State); + {receive_iq, IQ} -> case IQ of {iq, response, result, _, 'jabber:iq:roster', Payload, _, _, 'jabber:client'} -> diff --git a/src/core/ephraim_conn.erl b/src/core/ephraim_conn.erl index c4b163e..28e72b3 100644 --- a/src/core/ephraim_conn.erl +++ b/src/core/ephraim_conn.erl @@ -40,7 +40,12 @@ loop(State) -> #received_packet{packet_type=message, raw_packet=Packet} -> From = exmpp_xml:get_attribute(Packet, from, <<"unknown">>), - ephraim ! {receive_message, From, Packet}, + HasElement = exmpp_xml:has_element(Packet,'http://jabber.org/protocol/pubsub#event',event), + if HasElement -> + ephraim ! {receive_event, From, Packet}; + true -> + ephraim ! {receive_message, From, Packet} + end, loop(State); #received_packet{packet_type=iq, raw_packet=Packet} -> diff --git a/src/core/ephraim_conv.erl b/src/core/ephraim_conv.erl index 8f53098..b502166 100644 --- a/src/core/ephraim_conv.erl +++ b/src/core/ephraim_conv.erl @@ -17,8 +17,8 @@ loop(State) -> stop -> ok; - {receive_message, _Packet} -> - %io:format("Received packet from ~p: ~p~n", [State#conv_state.jid, Packet]), + {receive_message, Packet} -> + io:format("Received packet from ~p:~n~p~n", [State#conv_state.jid, Packet]), loop(State); Msg -> diff --git a/src/core/ephraim_event.erl b/src/core/ephraim_event.erl new file mode 100644 index 0000000..dd86a7b --- /dev/null +++ b/src/core/ephraim_event.erl @@ -0,0 +1,42 @@ +-module(ephraim_event). +-compile([debug_info, export_all]). +-include_lib("exmpp/include/exmpp.hrl"). + +-spec init() -> ok. +init() -> + loop(). + +-spec handleEvent(binary(), #xmlel{}) -> ok. +handleEvent(From, Item) -> + HasAvatarData = exmpp_xml:has_element(Item, 'urn:xmpp:avatar:data', data), + if HasAvatarData -> + Element = exmpp_xml:get_element(Item, 'urn:xmpp:avatar:data', data), + Data = base64:decode(exmpp_xml:get_cdata_from_list(Element#xmlel.children)), + ephraim ! {roster, {avatar, From, Data}}; + true -> + io:format("Received unhandled event from ~p:~n~p~n", [From, Item]) + end. + +-spec handleEvents(binary(), [#xmlel{}]) -> ok. +handleEvents(_, []) -> + ok; +handleEvents(From, [Item|Rest]) -> + handleEvent(From, Item), + handleEvents(From, Rest). + +-spec loop() -> ok. +loop() -> + receive + stop -> + ok; + + {receive_event, From, Packet} -> + Event = exmpp_xml:get_element(Packet, 'http://jabber.org/protocol/pubsub#event', event), + Items = exmpp_xml:get_element(Event, 'http://jabber.org/protocol/pubsub#event', items), + handleEvents(From, Items#xmlel.children), + loop(); + + Msg -> + io:format("ephraim_event: ~p~n", [Msg]), + loop() + end. diff --git a/src/core/ephraim_roster.erl b/src/core/ephraim_roster.erl index bfc40b1..2426761 100644 --- a/src/core/ephraim_roster.erl +++ b/src/core/ephraim_roster.erl @@ -6,11 +6,16 @@ entries :: dict() }). +-record(avatar, { + data :: binary() + }). + -record(roster_entry, { name :: binary() | undefined, subscription :: atom() | undefined, groups :: set(), - resources :: dict() + resources :: dict(), + avatar :: #avatar{} | undefined }). -record(resource_entry, { @@ -19,7 +24,6 @@ status :: binary() }). - -spec init() -> ok. init() -> loop(#roster{entries=dict:new()}). @@ -45,13 +49,11 @@ updateResource(Roster, JID, Priority, Type, Show, Status) -> _ -> RosterEntry#roster_entry.resources end, - Entries = dict:store(BareJID, RosterEntry#roster_entry{resources=Resources}, Roster#roster.entries), - if RosterEntry#roster_entry.subscription =:= undefined -> - ok; - true -> - ephraim ! {ui_update, {roster_update, BareJID, RosterEntry#roster_entry.name, RosterEntry#roster_entry.subscription, - sets:to_list(RosterEntry#roster_entry.groups), dict:to_list(Resources)}} - end, + NewEntry = RosterEntry#roster_entry{resources=Resources}, + + Entries = dict:store(BareJID, NewEntry, Roster#roster.entries), + uiUpdate(BareJID, NewEntry), + Roster#roster{entries=Entries}. -spec updateRosterEntry(#roster{}, binary(), binary(), atom(), set()) -> #roster{}. @@ -63,10 +65,36 @@ updateRosterEntry(Roster, JID, Name, Subscription, Groups) -> dict:new() end, NewEntry = #roster_entry{subscription=Subscription,name=Name,resources=Resources,groups=Groups}, + Entries = dict:store(JID, NewEntry, Roster#roster.entries), - ephraim ! {ui_update, {roster_update, JID, Name, Subscription, sets:to_list(Groups), dict:to_list(Resources)}}, + uiUpdate(JID, NewEntry), + Roster#roster{entries=Entries}. +-spec updateAvatar(#roster{}, binary(), binary()) -> #roster{}. +updateAvatar(Roster, JID, Avatar) -> + RosterEntry = case dict:find(JID, Roster#roster.entries) of + {ok, Entry} -> + Entry; + error -> + #roster_entry{resources=dict:new()} + end, + NewEntry = RosterEntry#roster_entry{avatar=#avatar{data=Avatar}}, + Entries = dict:store(JID, NewEntry, Roster#roster.entries), + uiUpdate(JID, NewEntry), + Roster#roster{entries=Entries}. + +-spec uiUpdate(binary(), #roster_entry{}) -> ok. +uiUpdate(JID, RosterEntry) -> + if RosterEntry#roster_entry.subscription =:= undefined -> + ok; + true -> + ephraim ! {ui_update, {roster_update, JID, RosterEntry#roster_entry.name, RosterEntry#roster_entry.subscription, + sets:to_list(RosterEntry#roster_entry.groups), dict:to_list(RosterEntry#roster_entry.resources), + RosterEntry#roster_entry.avatar}}, + ok + end. + -spec getGroups([#xmlel{}]) -> set(). getGroups(Els) -> getGroups(sets:new(), Els). @@ -116,6 +144,10 @@ loop(Roster) -> Roster2 = updateResource(Roster, From, Priority, Type, Show, Status), loop(Roster2); + {avatar, From, Avatar} -> + Roster2 = updateAvatar(Roster, From, Avatar), + loop(Roster2); + {roster_iq, Payload} -> Roster2 = handleRosterIQs(Roster, Payload#xmlel.children), %io:format("ephraim_roster: IQ: ~p~n", [Payload]), diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index def009b..6208e62 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -2,6 +2,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${ephraim_BINARY_DIR}) vala_precompile(VALA_C "Ephraim.vala" + "CellRendererAvatar.vala" "CellRendererContact.vala" "CellRendererPresence.vala" "Contact.vala" diff --git a/src/gui/CellRendererAvatar.vala b/src/gui/CellRendererAvatar.vala new file mode 100644 index 0000000..e4b53f4 --- /dev/null +++ b/src/gui/CellRendererAvatar.vala @@ -0,0 +1,19 @@ +public class CellRendererAvatar : Gtk.CellRendererPixbuf { + private Contact _contact; + + public Contact contact { + get { + return _contact; + } + set { + _contact = value; + + if(value == null) { + pixbuf = null; + return; + } + + pixbuf = value.avatar; + } + } +} diff --git a/src/gui/CellRendererContact.vala b/src/gui/CellRendererContact.vala index 7efe8cc..290ea09 100644 --- a/src/gui/CellRendererContact.vala +++ b/src/gui/CellRendererContact.vala @@ -8,7 +8,22 @@ public class CellRendererContact : Gtk.CellRendererText { set { _contact = value; - text = contact.display_string; + string str = escape_markup(contact.display_string); + + Gee.Map.Entry res = contact.get_resource_with_highest_priority(); + if(res != null && res.value.status != null) { + str += "\n" + escape_markup(res.value.status) + ""; + } + + markup = str; } } + + public CellRendererContact() { + ellipsize = Pango.EllipsizeMode.END; + } + + private static string escape_markup(string str) { + return str.replace("&", "&").replace("\"", """).replace("<", "<").replace(">", ">"); + } } diff --git a/src/gui/Contact.vala b/src/gui/Contact.vala index d79cfff..0e27b07 100644 --- a/src/gui/Contact.vala +++ b/src/gui/Contact.vala @@ -26,7 +26,9 @@ public class Contact : Object { public string jid {get; construct;} public string? name {get; construct;} public Subscription subscription {get; set;} + public Gdk.Pixbuf? avatar {get; set; default = null;} public Gee.TreeSet groups = new Gee.TreeSet(); + public string display_string {get; private set;} private Gee.HashMap resources = new Gee.HashMap(); diff --git a/src/gui/CoreConnector.vala b/src/gui/CoreConnector.vala index 1030428..9c492b7 100644 --- a/src/gui/CoreConnector.vala +++ b/src/gui/CoreConnector.vala @@ -90,7 +90,7 @@ public class CoreConnector { unowned Erl.Term term = store.term; Erl.Term match_term; - if((match_term = match("{roster_update,JID,Name,Subscription,Groups,Resources}", term)) != null) { + if((match_term = match("{roster_update,JID,Name,Subscription,Groups,Resources,Avatar}", term)) != null) { Erl.Term jid_term = match_term.var_content("JID"); if(!jid_term.is_binary()) // TODO Debug output @@ -151,6 +151,21 @@ public class CoreConnector { resources = resources.tl(); } + Erl.Term avatar = match("{avatar,Data}", match_term.var_content("Avatar")); + if(avatar != null) { + Erl.Term avatarData = avatar.var_content("Data"); + + if(avatarData.is_binary()) { + InputStream avatarStream = new MemoryInputStream.from_data(avatarData.bin_ptr(), avatarData.bin_size(), null); + try { + contact.avatar = new Gdk.Pixbuf.from_stream_at_scale(avatarStream, 32, 32, true, null); + avatarStream.close(null); + } catch(Error e) { + // TODO Debug output + } + } + } + roster.update_contact(contact); } else { diff --git a/src/gui/Ephraim.vala b/src/gui/Ephraim.vala index 6474482..5130247 100644 --- a/src/gui/Ephraim.vala +++ b/src/gui/Ephraim.vala @@ -25,13 +25,14 @@ public class Ephraim { rosterView.set_model(roster); rosterView.query_tooltip.connect((x, y, keyboard_tip, tooltip) => { + Gtk.TreeModel model; Gtk.TreeIter iter; - if(!rosterView.get_tooltip_context(out x, out y, keyboard_tip, null, null, out iter)) + if(!rosterView.get_tooltip_context(out x, out y, keyboard_tip, out model, null, out iter)) return false; Value value; - roster.get_value(iter, 0, out value); + model.get_value(iter, 0, out value); Contact? c = value.get_object() as Contact; if(c == null) @@ -42,14 +43,27 @@ public class Ephraim { if(r == null) return false; - tooltip.set_text("Resource: " + r.key); + tooltip.set_text("Resource: " + r.key + " (" + r.value.priority.to_string() + ")"); return true; }); rosterView.has_tooltip = true; - rosterView.append_column(new Gtk.TreeViewColumn.with_attributes("Presence", new CellRendererPresence(), "contact", 0, null)); - rosterView.append_column(new Gtk.TreeViewColumn.with_attributes("Contact", new CellRendererContact(), "contact", 0, null)); + Gtk.TreeViewColumn presenceColumn = new Gtk.TreeViewColumn.with_attributes(null, new CellRendererPresence(), "contact", 0, null); + presenceColumn.min_width = 32; + rosterView.append_column(presenceColumn); + + Gtk.TreeViewColumn contactColumn = new Gtk.TreeViewColumn(); + + Gtk.CellRenderer cellRendererContact = new CellRendererContact(); + contactColumn.pack_start(cellRendererContact, true); + contactColumn.set_attributes(cellRendererContact, "contact", 0, null); + + Gtk.CellRenderer cellRendererAvatar = new CellRendererAvatar(); + contactColumn.pack_end(cellRendererAvatar, false); + contactColumn.set_attributes(cellRendererAvatar, "contact", 0, null); + + rosterView.append_column(contactColumn); window.visible = true; diff --git a/src/gui/Roster.vala b/src/gui/Roster.vala index bd6b663..f25584c 100644 --- a/src/gui/Roster.vala +++ b/src/gui/Roster.vala @@ -80,8 +80,10 @@ public class Roster : Object, Gtk.TreeModel { } public void get_value (Gtk.TreeIter iter, int column, out Value value) { - if (column != 0 || iter.stamp != stamp || iter.user_data != this) + if (column != 0 || iter.stamp != stamp || iter.user_data != this) { + value = Value(Type.INVALID); return; + } Contact c = iter.user_data2 as Contact; value = Value(typeof(Contact)); -- cgit v1.2.3