1 #import <ObjXMPP/namespaces.h>
3 #include <gdk-pixbuf/gdk-pixbuf.h>
5 #import "JubGtkRosterUI.h"
6 #import "JubGObjectMap.h"
7 #import "JubGtkChatUI.h"
8 #import "JubGtkHelper.h"
10 static void roster_row_activated(GtkTreeView *tree_view, GtkTreePath *path,
11 GtkTreeViewColumn *column, gpointer data)
13 OFAutoreleasePool *pool;
15 JubChatClient *client = data;
17 GtkTreeModel *tree_model;
21 tree_model = gtk_tree_view_get_model(tree_view);
22 gtk_tree_model_get_iter(tree_model, &row_iter, path);
23 gtk_tree_model_get(tree_model, &row_iter, 1, &jid_s, -1);
25 // This was a group row
28 pool = [OFAutoreleasePool new];
30 jid = [OFString stringWithUTF8String: jid_s];
31 contact = [client.contactManager.contacts objectForKey: jid];
33 [client performSelectorOnMainThread: @selector(chatForContact:)
35 waitUntilDone: false];
39 static void presence_changed(GtkComboBox *combo_box, gpointer data)
41 JubChatClient *client = data;
42 OFAutoreleasePool *pool = [OFAutoreleasePool new];
43 const char *status = gtk_combo_box_get_active_id(combo_box);
45 [client sendPresenceWithStatus: @(status)];
50 static gboolean filter_roster_by_presence(GtkTreeModel *model,
51 GtkTreeIter *iter, gpointer data)
53 bool *showOffline = data;
55 gtk_tree_model_get(model, iter, 2, &status, -1);
57 // Groups have no status
61 if (!*showOffline && !strcmp(status, "unavailable"))
67 static void menu_show_offline(GtkCheckMenuItem *checkmenuitem, gpointer data)
69 JubGtkRosterUI *ui = data;
70 ui.showOffline = gtk_check_menu_item_get_active(checkmenuitem);
73 static void dialog_response_callback(GtkDialog *dialog, gint response_id,
76 void (^block)(gint) = user_data;
81 @implementation JubGtkRosterUI
82 - initWithClient: (JubChatClient*)client
87 GtkCheckMenuItem *show_offline_menu_item;
88 GtkTreeView *roster_view;
91 _groupMap = [[OFMapTable alloc]
92 initWithKeyFunctions: keyFunctions
93 valueFunctions: rowRefFunctions];
94 _subDialogMap = [[OFMapTable alloc]
95 initWithKeyFunctions: keyFunctions
96 valueFunctions: gObjectFunctions];
97 _contactMap = [[OFMutableDictionary alloc] init];
98 _client = [client retain];
100 [_client.contactManager addDelegate: self];
101 [_client.avatarManager setDelegate: self];
103 builder = gtk_builder_new();
104 gtk_builder_add_from_file(builder, "data/gtk/roster.ui", NULL);
105 gtk_builder_connect_signals(builder, NULL);
108 GTK_WIDGET(gtk_builder_get_object(builder, "RosterWindow"));
110 gtk_widget_show(_roster_window);
112 _roster_model = GTK_TREE_STORE(gtk_builder_get_object(builder,
115 _roster_filter = GTK_TREE_MODEL_FILTER(
116 gtk_builder_get_object(builder, "RosterTreeModelFilter"));
118 gtk_tree_model_filter_set_visible_func(_roster_filter,
119 filter_roster_by_presence, &_showOffline, NULL);
121 roster_view = GTK_TREE_VIEW(gtk_builder_get_object(builder,
124 g_signal_connect(roster_view, "row_activated",
125 G_CALLBACK(roster_row_activated), client);
127 _presence_combo = GTK_COMBO_BOX(gtk_builder_get_object(builder,
128 "PresenceComboBox"));
130 _presence_combo_changed_handler_id =
131 g_signal_connect(_presence_combo, "changed",
132 G_CALLBACK(presence_changed), client);
134 show_offline_menu_item =
135 GTK_CHECK_MENU_ITEM(gtk_builder_get_object(builder,
136 "showOfflineCheckMenuItem"));
138 g_signal_connect(show_offline_menu_item, "toggled",
139 G_CALLBACK(menu_show_offline), self);
141 g_object_unref(G_OBJECT(builder));
152 [_client.avatarManager setDelegate: nil];
153 [_client.contactManager removeDelegate: self];
154 [_subDialogMap release];
156 [_contactMap release];
159 gtk_widget_destroy(_roster_window);
164 - (void)Jub_closeSubscribeDialogForRosterItem: (XMPPRosterItem*)rosterItem
166 // Close subscripton dialogs, answered on another client
168 OFString *subscription = rosterItem.subscription;
169 OFString *bareJID = [rosterItem.JID bareJID];
171 if (([subscription isEqual: @"from"] ||
172 [subscription isEqual: @"both"]) &&
173 (dialog = [_subDialogMap valueForKey: bareJID]) != NULL)
174 gtk_dialog_response(dialog, GTK_RESPONSE_NONE);
177 /* Roster Delegate methods */
178 - (void)Jub_addRosterItem: (XMPPRosterItem*)item
179 group: (OFString*)group
181 GtkTreeIter group_iter, contact_iter;
182 GtkTreeRowReference *group_ref, *contact_ref;
183 GtkTreePath *group_path, *contact_path;
184 OFString *bareJID = [item.JID bareJID];
185 OFMapTable *contactRows;
187 if (!(contactRows = [_contactMap objectForKey: bareJID])) {
188 contactRows = [OFMapTable
189 mapTableWithKeyFunctions: keyFunctions
190 valueFunctions: rowRefFunctions];
192 [_contactMap setObject: contactRows
196 group_ref = [_groupMap valueForKey: group];
199 // Create new group row
200 gtk_tree_store_append(_roster_model, &group_iter, NULL);
201 gtk_tree_store_set(_roster_model, &group_iter,
202 0, [group UTF8String], -1);
204 group_path = gtk_tree_model_get_path(GTK_TREE_MODEL(
205 _roster_model), &group_iter);
207 group_ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(
208 _roster_model), group_path);
210 [_groupMap setValue: group_ref
213 // Get iter for existing group row
214 group_path = gtk_tree_row_reference_get_path(group_ref);
216 gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model),
217 &group_iter, group_path);
219 gtk_tree_path_free(group_path);
221 // Create new contact row
222 gtk_tree_store_append(_roster_model, &contact_iter, &group_iter);
224 gtk_tree_store_set(_roster_model, &contact_iter,
225 0, [item.name UTF8String],
226 1, [bareJID UTF8String],
227 2, "unavailable", -1);
228 else if (item.JID.node)
229 gtk_tree_store_set(_roster_model, &contact_iter,
230 0, [item.JID.node UTF8String],
231 1, [bareJID UTF8String],
232 2, "unavailable", -1);
234 gtk_tree_store_set(_roster_model, &contact_iter,
235 0, [item.JID.domain UTF8String],
236 1, [bareJID UTF8String],
237 2, "unavailable", -1);
239 contact_path = gtk_tree_model_get_path(GTK_TREE_MODEL(_roster_model),
242 contact_ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(_roster_model),
245 gtk_tree_path_free(contact_path);
247 [contactRows setValue: contact_ref
251 - (void)Jub_removeRosterItem: (XMPPRosterItem*)item
252 group: (OFString*)group
254 GtkTreeIter contact_iter, group_iter;
255 GtkTreePath *contact_path, *group_path;
256 GtkTreeRowReference *contact_ref, *group_ref;
257 OFString *bareJID = [item.JID bareJID];
258 OFMapTable *contactRows = [_contactMap objectForKey: bareJID];
260 contact_ref = [contactRows valueForKey: group];
261 contact_path = gtk_tree_row_reference_get_path(contact_ref);
262 gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model), &contact_iter,
264 gtk_tree_path_free(contact_path);
266 gtk_tree_store_remove(_roster_model, &contact_iter);
268 group_ref = [_groupMap valueForKey: group];
269 group_path = gtk_tree_row_reference_get_path(group_ref);
270 gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model), &group_iter,
273 if (!gtk_tree_model_iter_has_child(GTK_TREE_MODEL(_roster_model),
275 gtk_tree_store_remove(_roster_model, &group_iter);
276 [_groupMap removeValueForKey: group];
279 gtk_tree_path_free(group_path);
281 [contactRows removeValueForKey: group];
282 if([contactRows count] == 0)
283 [_contactMap removeObjectForKey: bareJID];
286 - (void)contactManager: (XMPPContactManager*)manager
287 didAddContact: (XMPPContact*)contact
289 XMPPRosterItem *rosterItem = contact.rosterItem;
290 OFArray *groups = rosterItem.groups;;
293 groups = @[ @"General" ];
295 for (OFString *group in groups)
296 [self performSelectorOnGLibThread: @selector(
297 Jub_addRosterItem:group:)
298 withObject: rosterItem
301 [self Jub_closeSubscribeDialogForRosterItem: rosterItem];
304 - (void)contactManager: (XMPPContactManager*)manager
305 didRemoveContact: (XMPPContact*)contact
307 XMPPRosterItem *rosterItem = contact.rosterItem;
308 OFArray *groups = rosterItem.groups;
311 groups = @[ @"General" ];
313 for (OFString *group in groups)
314 [self performSelectorOnGLibThread: @selector(
315 Jub_removeRosterItem:group:)
316 withObject: rosterItem
320 - (void)contactManager: (XMPPContactManager*)manager
321 didReceiveSubscriptionRequest: (XMPPPresence*)presence
323 XMPPJID *JID = presence.from;
324 OFString *bareJID = [JID bareJID];
325 OFString *message = [OFString stringWithFormat: @"<b>%@</b> would like "
326 @"to subscribe to your presence.", bareJID];
328 GtkWidget *dialog, *content_area, *label;
330 if ([_subDialogMap valueForKey: JID] != NULL)
333 dialog = gtk_dialog_new_with_buttons("Subscription Request",
334 GTK_WINDOW(_roster_window), GTK_DIALOG_DESTROY_WITH_PARENT,
335 "Accept", GTK_RESPONSE_ACCEPT,
336 "Deny", GTK_RESPONSE_REJECT, NULL);
338 content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
339 label = gtk_label_new(NULL);
340 gtk_label_set_markup(GTK_LABEL(label), [message UTF8String]);
341 gtk_container_add(GTK_CONTAINER(content_area), label);
343 g_signal_connect(dialog, "response",
344 G_CALLBACK(dialog_response_callback),
345 [^(gint response_id) {
346 if (response_id == GTK_RESPONSE_ACCEPT)
347 [manager sendSubscribedToJID: JID];
348 else if (response_id == GTK_RESPONSE_REJECT)
349 [manager sendUnsubscribedToJID: JID];
350 [_subDialogMap removeValueForKey: bareJID];
351 gtk_widget_destroy(GTK_WIDGET(dialog));
354 [_subDialogMap setValue: dialog
357 gtk_widget_show_all(dialog);
361 - (void)contact: (XMPPContact*)contact
362 willUpdateWithRosterItem: (XMPPRosterItem*)rosterItem;
364 [self Jub_closeSubscribeDialogForRosterItem: rosterItem];
366 // Remove contact from old set of groups
367 XMPPRosterItem *oldItem = contact.rosterItem;
368 OFArray *groups = oldItem.groups;
371 groups = @[ @"General" ];
373 for (OFString *group in groups)
374 [self performSelectorOnGLibThread: @selector(
375 Jub_removeRosterItem:group:)
379 // Add contact to new set of groups
380 groups = rosterItem.groups;
383 groups = @[ @"General" ];
385 for (OFString *group in groups)
386 [self performSelectorOnGLibThread: @selector(
387 Jub_addRosterItem:group:)
388 withObject: rosterItem
392 - (void)contact: (XMPPContact*)contact
393 didSendPresence: (XMPPPresence*)presence
395 OFDictionary *allPresences = [contact presences];
396 XMPPPresence *highPresence = [[[allPresences allObjects] sortedArray]
398 OFMutableString *tooltip =
399 [OFMutableString stringWithString: @"<b>Resources:</b>"];
401 [allPresences enumerateKeysAndObjectsUsingBlock:
402 ^(OFString *resource, XMPPPresence *pres, bool *stop) {
403 [tooltip appendString: @"\n"];
404 [tooltip appendString: resource];
405 if ([pres.type isEqual: @"available"]) {
406 if (pres.show != nil)
407 [tooltip appendFormat: @" (%@)", pres.show];
409 [tooltip appendString: @" (available)"];
411 [tooltip appendString: @" (unavailable)"];
414 [tooltip appendFormat: @": <i>%@</i>", pres.status];
420 GtkTreeRowReference *ref;
421 OFString *bareJID = [contact.rosterItem.JID bareJID];
422 OFMapTable *contactRows = [_contactMap objectForKey: bareJID];
423 OFArray *groups = contact.rosterItem.groups;;
426 groups = @[ @"General" ];
428 for (OFString *group in groups) {
429 ref = [contactRows valueForKey: group];
430 path = gtk_tree_row_reference_get_path(ref);
431 gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model),
433 gtk_tree_path_free(path);
435 if ([highPresence.type isEqual: @"available"]) {
436 if (highPresence.show != nil)
437 gtk_tree_store_set(_roster_model, &iter,
438 2, [highPresence.show UTF8String],
441 gtk_tree_store_set(_roster_model, &iter,
444 gtk_tree_store_set(_roster_model, &iter,
445 2, "unavailable", -1);
447 gtk_tree_store_set(_roster_model, &iter,
448 3, [tooltip UTF8String], -1);
451 gtk_tree_model_filter_refilter(_roster_filter);
455 - (void)contact: (XMPPContact*)contact
456 didSetAvatar: (OFString*)avatarFile
458 of_log(@"Got an avatar from %@", contact.rosterItem.JID);
462 GtkTreeRowReference *ref;
463 OFString *bareJID = [contact.rosterItem.JID bareJID];
464 OFMapTable *contactRows = [_contactMap objectForKey: bareJID];
465 OFArray *groups = contact.rosterItem.groups;;
468 gdk_pixbuf_new_from_file_at_size([avatarFile UTF8String],
472 groups = @[ @"General" ];
474 for (OFString *group in groups) {
475 ref = [contactRows valueForKey: group];
476 path = gtk_tree_row_reference_get_path(ref);
477 gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model),
479 gtk_tree_path_free(path);
481 gtk_tree_store_set(_roster_model, &iter,
484 g_object_unref(G_OBJECT(avatar));
488 - (void)client: (JubChatClient*)client_
489 didChangePresence: (XMPPPresence*)presence
491 OFString *tooltip = @"";
493 [[presence elementForName: @"show"
494 namespace: XMPP_NS_CLIENT] stringValue];
496 [[presence elementForName: @"status"
497 namespace: XMPP_NS_CLIENT] stringValue];
500 tooltip = [@"<b>Status:</b> " stringByAppendingString: status];
503 // Block the PresenceComboBox's changed handler, so it doesn't
504 // fire and resend presence
505 g_signal_handler_block(_presence_combo,
506 _presence_combo_changed_handler_id);
508 if ([presence.type isEqual: @"unavailable"])
509 gtk_combo_box_set_active_id(_presence_combo,
511 else if (show == nil)
512 gtk_combo_box_set_active_id(_presence_combo,
515 gtk_combo_box_set_active_id(_presence_combo,
518 // Unblock the changed handler
519 g_signal_handler_unblock(_presence_combo,
520 _presence_combo_changed_handler_id);
522 gtk_widget_set_tooltip_markup(GTK_WIDGET(_presence_combo),
523 [tooltip UTF8String]);
529 OF_GETTER(_showOffline, true);
532 - (void)setShowOffline: (bool)showOffline
534 _showOffline = showOffline;
536 gtk_tree_model_filter_refilter(_roster_filter);