<column type="gchararray"/>
<!-- column-name status -->
<column type="gchararray"/>
+ <!-- column-name avatar -->
+ <column type="GdkPixbuf"/>
</columns>
</object>
<object class="GtkTreeModelFilter" id="RosterTreeModelFilter">
</child>
</object>
</child>
+ <child>
+ <object class="GtkTreeViewColumn" id="RosterTreeViewColumn3">
+ <property name="title" translatable="yes">Avatar</property>
+ <child>
+ <object class="GtkCellRendererPixbuf" id="cellrendererpixbuf1"/>
+ <attributes>
+ <attribute name="pixbuf">4</attribute>
+ </attributes>
+ </child>
+ </object>
+ </child>
</object>
</child>
</object>
--- /dev/null
+#import <ObjFW/ObjFW.h>
+#import <ObjXMPP/ObjXMPP.h>
+
+@class JubChatClient;
+@class XMPPContact;
+
+@protocol JubAvatarManagerDelegate
+- (void)contact: (XMPPContact*)contact
+ didSetAvatar: (OFString*)avatarFile;
+@end
+
+@interface JubAvatarManager : OFObject <XMPPConnectionDelegate>
+{
+ JubChatClient *_client;
+ id<JubAvatarManagerDelegate> _delegate;
+ OFMutableString *cachePath;
+}
+@property (assign) id<JubAvatarManagerDelegate> delegate;
+
+- initWithClient: (JubChatClient*)client;
+- (void)Jub_connection: (XMPPConnection*)connection
+ receivedAvatarIQ: (XMPPIQ*)IQ;
+@end
--- /dev/null
+#import "JubAvatarManager.h"
+#import "JubChatClient.h"
+
+#define JUB_NS_PUBSUB @"http://jabber.org/protocol/pubsub"
+#define JUB_NS_PUBSUB_EVENT @"http://jabber.org/protocol/pubsub#event"
+#define JUB_NS_AVATAR_DATA @"urn:xmpp:avatar:data"
+#define JUB_NS_AVATAR_METADATA @"urn:xmpp:avatar:metadata"
+
+@implementation JubAvatarManager
+@synthesize delegate = _delegate;
+
+- initWithClient: (JubChatClient*)client
+{
+ self = [super init];
+
+ @try {
+ _client = client;
+ [_client.discoEntity
+ addFeature: JUB_NS_AVATAR_METADATA @"+notify"];
+ [_client.connection addDelegate: self];
+
+ // Determine cache path
+ OFDictionary *env = [OFApplication environment];
+ OFString * xdgCache = env[@"XDG_CACHE_HOME"];
+
+ cachePath = [OFMutableString new];
+ if (xdgCache == nil) {
+ OFString *home = env[@"HOME"];
+ if (home == nil)
+ [cachePath appendString: @"/tmp"];
+ else
+ [cachePath appendString: home];
+ [cachePath appendString: @"/.cache"];
+ } else
+ [cachePath appendString: xdgCache];
+
+ [cachePath appendString: @"/jubjub"];
+ } @catch (id e) {
+ [self release];
+ @throw e;
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ // TODO: Remove feature
+ [cachePath release];
+ [_client.connection removeDelegate: self];
+ [super dealloc];
+}
+
+- (void)connection: (XMPPConnection*)connection
+ didReceiveMessage: (XMPPMessage*)message
+{
+ OFXMLElement *event = [message elementForName: @"event"
+ namespace: JUB_NS_PUBSUB_EVENT];
+ if (event == nil)
+ return;
+
+ OFXMLElement *items = [event elementForName: @"items"
+ namespace: JUB_NS_PUBSUB_EVENT];
+ if (items == nil ||
+ ![[[items attributeForName: @"node"] stringValue]
+ isEqual: JUB_NS_AVATAR_METADATA])
+ return;
+
+ OFXMLElement *item = [items elementForName: @"item"
+ namespace: JUB_NS_PUBSUB_EVENT];
+ if (item == nil)
+ return;
+
+ OFString *ID = [[item attributeForName: @"id"] stringValue];
+ if (ID == nil)
+ return;
+
+ OFXMLElement *metadata = [item elementForName: @"metadata"
+ namespace: JUB_NS_AVATAR_METADATA];
+ if (metadata == nil)
+ return;
+
+ OFXMLElement *info = [metadata elementForName: @"info"
+ namespace: JUB_NS_AVATAR_METADATA];
+ if (info == nil)
+ return;
+
+ OFString *avatarPath =
+ [cachePath stringByAppendingFormat: @"/%@.png", ID];
+ if ([OFFile fileExistsAtPath: avatarPath]) {
+ OFString *contactJID = [message.from bareJID];
+ XMPPContact *contact =
+ [_client.contactManager.contacts objectForKey: contactJID];
+ if (contact == nil) // Avatar for unknown contact
+ return;
+ [_delegate contact: contact
+ didSetAvatar: avatarPath];
+ return;
+ }
+
+ XMPPIQ *queryIQ =
+ [XMPPIQ IQWithType: @"get"
+ ID: [_client.connection generateStanzaID]];
+ queryIQ.to = message.from;
+
+ OFXMLElement *pubsub = [OFXMLElement elementWithName: @"pubsub"
+ namespace: JUB_NS_PUBSUB];
+ [queryIQ addChild: pubsub];
+
+ OFXMLElement *queryItems =
+ [OFXMLElement elementWithName: @"items"
+ namespace: JUB_NS_PUBSUB];
+ [queryItems addAttributeWithName: @"node"
+ stringValue: JUB_NS_AVATAR_DATA];
+ [pubsub addChild: queryItems];
+
+ OFXMLElement *queryItem =
+ [OFXMLElement elementWithName: @"item"
+ namespace: JUB_NS_PUBSUB];
+ [queryItem addAttributeWithName: @"id"
+ stringValue: ID];
+ [queryItems addChild: queryItem];
+
+ [_client.connection sendIQ: queryIQ
+ callbackTarget: self
+ selector: @selector(Jub_connection:
+ receivedAvatarIQ:)];
+}
+
+- (void)Jub_connection: (XMPPConnection*)connection
+ receivedAvatarIQ: (XMPPIQ*)IQ
+{
+ OFXMLElement *pubsub = [IQ elementForName: @"pubsub"
+ namespace: JUB_NS_PUBSUB];
+ if (pubsub == nil)
+ return;
+
+ OFXMLElement *items = [pubsub elementForName: @"items"
+ namespace: JUB_NS_PUBSUB];
+ if (items == nil ||
+ ![[[items attributeForName: @"node"] stringValue]
+ isEqual: JUB_NS_AVATAR_DATA])
+ return;
+
+ OFXMLElement *item = [items elementForName: @"item"
+ namespace: JUB_NS_PUBSUB];
+ if (item == nil)
+ return;
+
+ OFString *ID = [[item attributeForName: @"id"] stringValue];
+ if (ID == nil)
+ return;
+
+ OFXMLElement *data = [item elementForName: @"data"
+ namespace: JUB_NS_AVATAR_DATA];
+ if (data == nil)
+ return;
+
+ OFDataArray *avatar =
+ [OFDataArray dataArrayWithBase64EncodedString: [data stringValue]];
+
+ if (![OFFile directoryExistsAtPath: cachePath])
+ [OFFile createDirectoryAtPath: cachePath
+ createParents: true];
+
+ OFString *filename =
+ [cachePath stringByAppendingFormat: @"/%@.png", ID];
+ [avatar writeToFile: filename];
+
+ OFString *contactJID = [IQ.from bareJID];
+
+ XMPPContact *contact =
+ [_client.contactManager.contacts objectForKey: contactJID];
+ if (contact == nil) // Avatar for unknown contact
+ return;
+
+ [_delegate contact: contact
+ didSetAvatar: filename];
+}
+@end
#import "JubChatUI.h"
#import "JubConfig.h"
+@class JubAvatarManager;
+
@interface JubChatClient : OFObject
<XMPPConnectionDelegate, XMPPRosterDelegate, XMPPContactManagerDelegate>
{
XMPPConnection *_connection;
XMPPRoster *_roster;
XMPPStreamManagement *_streamManagement;
+ JubAvatarManager *_avatarManager;
XMPPContactManager *_contactManager;
XMPPDiscoEntity *_discoEntity;
XMPPPresence *_presence;
}
@property (readonly) XMPPConnection *connection;
@property (readonly) XMPPRoster *roster;
+@property (readonly) JubAvatarManager *avatarManager;
@property (readonly) XMPPContactManager *contactManager;
@property (readonly) XMPPDiscoEntity *discoEntity;
@property (readonly) XMPPPresence *presence;
#import "JubChatClient.h"
#import "ObjXMPP/namespaces.h"
+#import "JubAvatarManager.h"
+
#define JUB_CLIENT_URI @"http://babelmonkeys.de/jubjub"
@implementation JubChatClient
@synthesize connection = _connection;
@synthesize roster = _roster;
+@synthesize avatarManager = _avatarManager;
@synthesize contactManager = _contactManager;
@synthesize discoEntity = _discoEntity;
@synthesize presence = _presence;
_roster = [[XMPPRoster alloc] initWithConnection: _connection];
[_roster addDelegate: self];
+ _discoEntity =
+ [[XMPPDiscoEntity alloc] initWithConnection: _connection
+ capsNode: JUB_CLIENT_URI];
+
+ XMPPDiscoIdentity *identity =
+ [XMPPDiscoIdentity identityWithCategory: @"client"
+ type: @"pc"
+ name: @"JubJub"];
+ [_discoEntity addIdentity: identity];
+ [_discoEntity addFeature: XMPP_NS_CAPS];
+
+ _avatarManager =
+ [[JubAvatarManager alloc] initWithClient: self];
+
_contactManager = [[XMPPContactManager alloc]
initWithConnection: _connection
roster: _roster];
[_contactManager release];
[_discoEntity release];
[_streamManagement release];
+ [_avatarManager release];
[_connection release];
[_presence release];
[_chatMap release];
- (void)connection: (XMPPConnection*)connection
wasBoundToJID: (XMPPJID*)jid
{
- of_log(@"Bound to JID: %@", [jid fullJID]);
-
- _discoEntity =
- [[XMPPDiscoEntity alloc] initWithConnection: connection
- capsNode: JUB_CLIENT_URI];
-
- XMPPDiscoIdentity *identity =
- [XMPPDiscoIdentity identityWithCategory: @"client"
- type: @"pc"
- name: @"JubJub"];
- [_discoEntity addIdentity: identity];
- [_discoEntity addFeature: XMPP_NS_CAPS];
-
[_roster requestRoster];
}
STATIC_LIB_NOINST = core.a
-SRCS = main.m \
- JubChatClient.m \
+SRCS = main.m \
+ JubAvatarManager.m \
+ JubChatClient.m \
JubConfig.m
include ../../buildsys.mk
_client.ui = _ui;
[_client.connection addDelegate: self];
- [_client.connection asyncConnectAndHandle];
-
[_ui startUIThread];
+ [_client.connection asyncConnectAndHandle];
}
- (void)connection: (XMPPConnection*)connection
#include <gtk/gtk.h>
#import "JubChatClient.h"
+#import "JubAvatarManager.h"
@class JubGtkChatUI;
-@interface JubGtkRosterUI: OFObject <XMPPContactManagerDelegate>
+@interface JubGtkRosterUI:
+ OFObject <XMPPContactManagerDelegate, JubAvatarManagerDelegate>
{
GtkWidget *_roster_window;
GtkTreeStore *_roster_model;
#import <ObjXMPP/namespaces.h>
#include <string.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
#import "JubGtkRosterUI.h"
#import "JubGObjectMap.h"
_client = [client retain];
[_client.contactManager addDelegate: self];
+ [_client.avatarManager setDelegate: self];
builder = gtk_builder_new();
gtk_builder_add_from_file(builder, "data/gtk/roster.ui", NULL);
- (void)dealloc
{
+ [_client.avatarManager setDelegate: nil];
[_client.contactManager removeDelegate: self];
[_groupMap release];
[_contactMap release];
});
}
+- (void)contact: (XMPPContact*)contact
+ didSetAvatar: (OFString*)avatarFile
+{
+ of_log(@"Got an avatar from %@", contact.rosterItem.JID);
+ g_idle_add_block(^{
+ GtkTreeIter iter;
+ GtkTreePath *path;
+ GtkTreeRowReference *ref;
+ OFString *bareJID = [contact.rosterItem.JID bareJID];
+ OFMapTable *contactRows = [_contactMap objectForKey: bareJID];
+ OFArray *groups = contact.rosterItem.groups;;
+
+ GdkPixbuf *avatar =
+ gdk_pixbuf_new_from_file([avatarFile UTF8String], NULL);
+
+ if (groups == nil)
+ groups = @[ @"General" ];
+
+ for (OFString *group in groups) {
+ ref = [contactRows valueForKey: group];
+ path = gtk_tree_row_reference_get_path(ref);
+ gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model),
+ &iter, path);
+ gtk_tree_path_free(path);
+
+ gtk_tree_store_set(_roster_model, &iter,
+ 4, avatar, -1);
+ }
+ g_object_unref(G_OBJECT(avatar));
+ });
+}
+
- (void)client: (JubChatClient*)client_
didChangePresence: (XMPPPresence*)presence
{