]> git.0d.be Git - empathy.git/commitdiff
Base the contact list around libfolks metacontacts. Not yet to feature-parity
authorTravis Reitter <treitter@gmail.com>
Fri, 28 May 2010 23:13:51 +0000 (16:13 -0700)
committerTravis Reitter <treitter@gmail.com>
Tue, 20 Jul 2010 23:12:35 +0000 (16:12 -0700)
with mainline Empathy.

15 files changed:
configure.ac
libempathy-gtk/Makefile.am
libempathy-gtk/empathy-individual-menu.h [new file with mode: 0644]
libempathy-gtk/empathy-individual-store.c [new file with mode: 0644]
libempathy-gtk/empathy-individual-store.h [new file with mode: 0644]
libempathy-gtk/empathy-individual-view.c [new file with mode: 0644]
libempathy-gtk/empathy-individual-view.h [new file with mode: 0644]
libempathy-gtk/empathy-ui-utils.c
libempathy-gtk/empathy-ui-utils.h
libempathy/Makefile.am
libempathy/empathy-individual-manager.c [new file with mode: 0644]
libempathy/empathy-individual-manager.h [new file with mode: 0644]
libempathy/empathy-utils.c
libempathy/empathy-utils.h
src/empathy-main-window.c

index 128628f34d4496cb5cf7c1d33e44041904b1e5c7..cadebc867dcc7500450e9336ac0519a9bc66bddf 100644 (file)
@@ -31,6 +31,7 @@ AC_COPYRIGHT([
 # Minimal version required
 
 # Hardp deps
+FOLKS_REQUIRED=0.1.0
 GCONF_REQUIRED=1.2.0
 GLIB_REQUIRED=2.25.9
 GTK_REQUIRED=2.21.2
@@ -139,6 +140,31 @@ AS_COMPILER_FLAG(-Wmissing-declarations, ERROR_CFLAGS="$ERROR_CFLAGS -Wmissing-d
 
 AC_SUBST(ERROR_CFLAGS)
 
+# -----------------------------------------------------------
+# Enable TPL
+# -----------------------------------------------------------
+
+# It needs to be defined before PKG_CHECK_MODULES calls
+AC_ARG_ENABLE(tpl,
+  AC_HELP_STRING([--enable-tpl],[enable telepathy-logger code and disable the
+    empathy logger]), enable_tpl=$enableval, enable_tpl=no )
+AM_CONDITIONAL(ENABLE_TPL, test "x$enable_tpl" = "xyes")
+
+if test x${enable_tpl} = xyes; then
+  AC_DEFINE(ENABLE_TPL, [], [Enable TPL code])
+fi
+
+if test "x$enable_tpl" = "xyes"; then
+  PKG_CHECK_MODULES(TPL,
+  [
+     telepathy-logger = $TELEPATHY_LOGGER
+  ])
+  AC_SUBST(TPL_CFLAGS)
+  AC_SUBST(TPL_LIBS)
+fi
+
+
+
 # -----------------------------------------------------------
 # Pkg-Config dependency checks
 # -----------------------------------------------------------
@@ -147,6 +173,8 @@ PKG_CHECK_MODULES(EMPATHY,
 [
    dbus-glib-1
    farsight2-0.10
+   folks >= $FOLKS_REQUIRED
+   folks-telepathy >= $FOLKS_REQUIRED
    gconf-2.0 >= $GCONF_REQUIRED
    gio-2.0 >= $GLIB_REQUIRED
    gio-unix-2.0 >= $GLIB_REQUIRED
index 6fa8202ddc6fb4a4c904630a9c16d6054319dc48..6959578ae7486002980e3d8517ac1297b478c053 100644 (file)
@@ -52,6 +52,8 @@ libempathy_gtk_handwritten_source =                   \
        empathy-contact-selector-dialog.c \
        empathy-contact-widget.c                \
        empathy-geometry.c                      \
+       empathy-individual-store.c              \
+       empathy-individual-view.c               \
        empathy-irc-network-dialog.c            \
        empathy-kludge-label.c                  \
        empathy-log-window.c                    \
@@ -99,6 +101,9 @@ libempathy_gtk_headers =                     \
        empathy-contact-widget.h                \
        empathy-geometry.h                      \
        empathy-images.h                        \
+       empathy-individual-menu.h               \
+       empathy-individual-store.h              \
+       empathy-individual-view.h               \
        empathy-irc-network-dialog.h            \
        empathy-kludge-label.h                  \
        empathy-log-window.h                    \
diff --git a/libempathy-gtk/empathy-individual-menu.h b/libempathy-gtk/empathy-individual-menu.h
new file mode 100644 (file)
index 0000000..5d22897
--- /dev/null
@@ -0,0 +1,44 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2008-2010 Collabora Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Authors: Xavier Claessens <xclaesse@gmail.com>
+ *          Travis Reitter <travis.reitter@collabora.co.uk>
+ */
+
+#ifndef __EMPATHY_INDIVIDUAL_MENU_H__
+#define __EMPATHY_INDIVIDUAL_MENU_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+       EMPATHY_INDIVIDUAL_FEATURE_NONE = 0,
+       EMPATHY_INDIVIDUAL_FEATURE_CHAT = 1 << 0,
+       EMPATHY_INDIVIDUAL_FEATURE_CALL = 1 << 1,
+       EMPATHY_INDIVIDUAL_FEATURE_LOG = 1 << 2,
+       EMPATHY_INDIVIDUAL_FEATURE_EDIT = 1 << 3,
+       EMPATHY_INDIVIDUAL_FEATURE_INFO = 1 << 4,
+       EMPATHY_INDIVIDUAL_FEATURE_FAVOURITE = 1 << 5,
+       EMPATHY_INDIVIDUAL_FEATURE_ALL = (1 << 6) - 1,
+} EmpathyIndividualFeatureFlags;
+
+G_END_DECLS
+
+#endif /* __EMPATHY_INDIVIDUAL_MENU_H__ */
+
diff --git a/libempathy-gtk/empathy-individual-store.c b/libempathy-gtk/empathy-individual-store.c
new file mode 100644 (file)
index 0000000..4fed77a
--- /dev/null
@@ -0,0 +1,1830 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2005-2007 Imendio AB
+ * Copyright (C) 2007-2010 Collabora Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Authors: Mikael Hallendal <micke@imendio.com>
+ *          Martyn Russell <martyn@imendio.com>
+ *          Xavier Claessens <xclaesse@gmail.com>
+ *          Travis Reitter <travis.reitter@collabora.co.uk>
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <gtk/gtk.h>
+
+#include <folks/folks.h>
+#include <folks/folks-telepathy.h>
+#include <telepathy-glib/util.h>
+
+#include <libempathy/empathy-utils.h>
+#include <libempathy/empathy-enum-types.h>
+#include <libempathy/empathy-individual-manager.h>
+
+#include "empathy-individual-store.h"
+#include "empathy-ui-utils.h"
+#include "empathy-gtk-enum-types.h"
+
+#define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
+#include <libempathy/empathy-debug.h>
+
+/* Active users are those which have recently changed state
+ * (e.g. online, offline or from normal to a busy state).
+ */
+
+/* Time in seconds user is shown as active */
+#define ACTIVE_USER_SHOW_TIME 7
+
+/* Time in seconds after connecting which we wait before active users are enabled */
+#define ACTIVE_USER_WAIT_TO_ENABLE_TIME 5
+
+#define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyIndividualStore)
+typedef struct
+{
+  EmpathyIndividualManager *manager;
+  gboolean show_offline;
+  gboolean show_avatars;
+  gboolean show_groups;
+  gboolean is_compact;
+  gboolean show_protocols;
+  gboolean show_active;
+  EmpathyIndividualStoreSort sort_criterium;
+  guint inhibit_active;
+  guint setup_idle_id;
+  gboolean dispose_has_run;
+  GHashTable *status_icons;
+} EmpathyIndividualStorePriv;
+
+typedef struct
+{
+  GtkTreeIter iter;
+  const gchar *name;
+  gboolean found;
+} FindGroup;
+
+typedef struct
+{
+  FolksIndividual *individual;
+  gboolean found;
+  GList *iters;
+} FindContact;
+
+typedef struct
+{
+  EmpathyIndividualStore *self;
+  FolksIndividual *individual;
+  gboolean remove;
+} ShowActiveData;
+
+enum
+{
+  PROP_0,
+  PROP_INDIVIDUAL_MANAGER,
+  PROP_SHOW_OFFLINE,
+  PROP_SHOW_AVATARS,
+  PROP_SHOW_PROTOCOLS,
+  PROP_SHOW_GROUPS,
+  PROP_IS_COMPACT,
+  PROP_SORT_CRITERIUM
+};
+
+/* prototypes to break cycles */
+static void individual_store_contact_update (EmpathyIndividualStore *self,
+    FolksIndividual *individual);
+
+G_DEFINE_TYPE (EmpathyIndividualStore, empathy_individual_store,
+    GTK_TYPE_TREE_STORE);
+
+static void
+add_individual_to_store (GtkTreeStore *self,
+    GtkTreeIter *iter,
+    FolksIndividual *individual,
+    EmpathyIndividualManagerFlags flags)
+{
+  gtk_tree_store_set (self, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME,
+      folks_individual_get_alias (individual),
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, individual,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, FALSE,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, FALSE,
+      EMPATHY_INDIVIDUAL_STORE_COL_CAN_AUDIO_CALL,
+      folks_individual_get_capabilities (individual) &
+      FOLKS_CAPABILITIES_FLAGS_AUDIO,
+      EMPATHY_INDIVIDUAL_STORE_COL_CAN_VIDEO_CALL,
+      folks_individual_get_capabilities (individual) &
+      FOLKS_CAPABILITIES_FLAGS_VIDEO,
+      EMPATHY_INDIVIDUAL_STORE_COL_FLAGS, flags, -1);
+}
+
+static gboolean
+individual_store_get_group_foreach (GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkTreeIter *iter,
+    FindGroup *fg)
+{
+  gchar *str;
+  gboolean is_group;
+
+  /* Groups are only at the top level. */
+  if (gtk_tree_path_get_depth (path) != 1)
+    {
+      return FALSE;
+    }
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &str,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group, -1);
+
+  if (is_group && !tp_strdiff (str, fg->name))
+    {
+      fg->found = TRUE;
+      fg->iter = *iter;
+    }
+
+  g_free (str);
+
+  return fg->found;
+}
+
+static void
+individual_store_get_group (EmpathyIndividualStore *self,
+    const gchar *name,
+    GtkTreeIter *iter_group_to_set,
+    GtkTreeIter *iter_separator_to_set,
+    gboolean *created,
+    gboolean is_fake_group)
+{
+  EmpathyIndividualStorePriv *priv;
+  GtkTreeModel *model;
+  GtkTreeIter iter_group;
+  GtkTreeIter iter_separator;
+  FindGroup fg;
+
+  priv = GET_PRIV (self);
+
+  memset (&fg, 0, sizeof (fg));
+
+  fg.name = name;
+
+  model = GTK_TREE_MODEL (self);
+  gtk_tree_model_foreach (model,
+      (GtkTreeModelForeachFunc) individual_store_get_group_foreach, &fg);
+
+  if (!fg.found)
+    {
+      if (created)
+        {
+          *created = TRUE;
+        }
+
+      gtk_tree_store_append (GTK_TREE_STORE (self), &iter_group, NULL);
+      gtk_tree_store_set (GTK_TREE_STORE (self), &iter_group,
+          EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS, NULL,
+          EMPATHY_INDIVIDUAL_STORE_COL_NAME, name,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, TRUE,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, FALSE,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, FALSE,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, is_fake_group, -1);
+
+      if (iter_group_to_set)
+        {
+          *iter_group_to_set = iter_group;
+        }
+
+      gtk_tree_store_append (GTK_TREE_STORE (self),
+          &iter_separator, &iter_group);
+      gtk_tree_store_set (GTK_TREE_STORE (self), &iter_separator,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, TRUE, -1);
+
+      if (iter_separator_to_set)
+        {
+          *iter_separator_to_set = iter_separator;
+        }
+    }
+  else
+    {
+      if (created)
+        {
+          *created = FALSE;
+        }
+
+      if (iter_group_to_set)
+        {
+          *iter_group_to_set = fg.iter;
+        }
+
+      iter_separator = fg.iter;
+
+      if (gtk_tree_model_iter_next (model, &iter_separator))
+        {
+          gboolean is_separator;
+
+          gtk_tree_model_get (model, &iter_separator,
+              EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator, -1);
+
+          if (is_separator && iter_separator_to_set)
+            {
+              *iter_separator_to_set = iter_separator;
+            }
+        }
+    }
+}
+
+static gboolean
+individual_store_find_contact_foreach (GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkTreeIter *iter,
+    FindContact *fc)
+{
+  FolksIndividual *individual;
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
+
+  if (individual == fc->individual)
+    {
+      fc->found = TRUE;
+      fc->iters = g_list_append (fc->iters, gtk_tree_iter_copy (iter));
+    }
+
+  if (individual)
+    {
+      g_object_unref (individual);
+    }
+
+  return FALSE;
+}
+
+static GList *
+individual_store_find_contact (EmpathyIndividualStore *self,
+    FolksIndividual *individual)
+{
+  EmpathyIndividualStorePriv *priv;
+  GtkTreeModel *model;
+  GList *l = NULL;
+  FindContact fc;
+
+  priv = GET_PRIV (self);
+
+  memset (&fc, 0, sizeof (fc));
+
+  fc.individual = individual;
+
+  model = GTK_TREE_MODEL (self);
+  gtk_tree_model_foreach (model,
+      (GtkTreeModelForeachFunc) individual_store_find_contact_foreach, &fc);
+
+  if (fc.found)
+    {
+      l = fc.iters;
+    }
+
+  return l;
+}
+
+static void
+individual_store_remove_individual (EmpathyIndividualStore *self,
+    FolksIndividual *individual)
+{
+  EmpathyIndividualStorePriv *priv;
+  GtkTreeModel *model;
+  GList *iters, *l;
+
+  priv = GET_PRIV (self);
+
+  iters = individual_store_find_contact (self, individual);
+  if (!iters)
+    {
+      return;
+    }
+
+  /* Clean up model */
+  model = GTK_TREE_MODEL (self);
+
+  for (l = iters; l; l = l->next)
+    {
+      GtkTreeIter parent;
+
+      /* NOTE: it is only <= 2 here because we have
+       * separators after the group name, otherwise it
+       * should be 1.
+       */
+      if (gtk_tree_model_iter_parent (model, &parent, l->data) &&
+          gtk_tree_model_iter_n_children (model, &parent) <= 2)
+        {
+          gtk_tree_store_remove (GTK_TREE_STORE (self), &parent);
+        }
+      else
+        {
+          gtk_tree_store_remove (GTK_TREE_STORE (self), l->data);
+        }
+    }
+
+  g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
+  g_list_free (iters);
+}
+
+static void
+individual_store_add_individual (EmpathyIndividualStore *self,
+    FolksIndividual *individual)
+{
+  EmpathyIndividualStorePriv *priv;
+  GtkTreeIter iter;
+  GHashTable *group_set = NULL;
+  GList *groups = NULL, *l;
+  EmpathyIndividualManagerFlags flags = 0;
+
+  priv = GET_PRIV (self);
+
+  if (EMP_STR_EMPTY (folks_individual_get_alias (individual)) ||
+      (!priv->show_offline && !folks_individual_is_online (individual)))
+    {
+      return;
+    }
+
+  if (priv->show_groups)
+    {
+      group_set = folks_individual_get_groups (individual);
+      groups = g_hash_table_get_keys (group_set);
+    }
+
+  /* TODO: implement */
+  DEBUG ("group capability flags not implemented");
+
+  if (!groups)
+    {
+      GtkTreeIter iter_group, *parent;
+
+      parent = &iter_group;
+
+      /* TODO: implement */
+      DEBUG ("forcing the People Nearby group even when 'show "
+          "groups' is off is unimplemented");
+
+      if (!priv->show_groups)
+        {
+          parent = NULL;
+        }
+      else
+        {
+          individual_store_get_group (self,
+              EMPATHY_INDIVIDUAL_STORE_UNGROUPED,
+              &iter_group, NULL, NULL, TRUE);
+        }
+
+      gtk_tree_store_insert_after (GTK_TREE_STORE (self), &iter,
+          parent, NULL);
+
+      add_individual_to_store (GTK_TREE_STORE (self), &iter,
+          individual, flags);
+    }
+
+  /* Else add to each group. */
+  for (l = groups; l; l = l->next)
+    {
+      GtkTreeIter iter_group;
+
+      individual_store_get_group (self, l->data, &iter_group, NULL, NULL,
+          FALSE);
+
+      gtk_tree_store_insert_after (GTK_TREE_STORE (self), &iter,
+          &iter_group, NULL);
+
+      add_individual_to_store (GTK_TREE_STORE (self), &iter,
+          individual, flags);
+    }
+  g_list_free (groups);
+  if (group_set != NULL)
+    g_hash_table_unref (group_set);
+
+#ifdef HAVE_FAVOURITE_CONTACTS
+  if (priv->show_groups &&
+      empathy_individual_manager_is_favourite (priv->manager, individual))
+    {
+      /* Add contact to the fake 'Favorites' group */
+      GtkTreeIter iter_group;
+
+      individual_store_get_group (self, EMPATHY_INDIVIDUAL_STORE_FAVORITE,
+          &iter_group, NULL, NULL, TRUE);
+
+      gtk_tree_store_insert_after (GTK_TREE_STORE (self), &iter,
+          &iter_group, NULL);
+
+      add_individual_to_store (GTK_TREE_STORE (self), &iter,
+          individual, flags);
+    }
+#endif
+
+  individual_store_contact_update (self, individual);
+}
+
+static void
+individual_store_contact_set_active (EmpathyIndividualStore *self,
+    FolksIndividual *individual,
+    gboolean active,
+    gboolean set_changed)
+{
+  EmpathyIndividualStorePriv *priv;
+  GtkTreeModel *model;
+  GList *iters, *l;
+
+  priv = GET_PRIV (self);
+  model = GTK_TREE_MODEL (self);
+
+  iters = individual_store_find_contact (self, individual);
+  for (l = iters; l; l = l->next)
+    {
+      GtkTreePath *path;
+
+      gtk_tree_store_set (GTK_TREE_STORE (self), l->data,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, active, -1);
+
+      DEBUG ("Set item %s", active ? "active" : "inactive");
+
+      if (set_changed)
+        {
+          path = gtk_tree_model_get_path (model, l->data);
+          gtk_tree_model_row_changed (model, path, l->data);
+          gtk_tree_path_free (path);
+        }
+    }
+
+  g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
+  g_list_free (iters);
+
+}
+
+static ShowActiveData *
+individual_store_contact_active_new (EmpathyIndividualStore *self,
+    FolksIndividual *individual,
+    gboolean remove_)
+{
+  ShowActiveData *data;
+
+  DEBUG ("Individual'%s' now active, and %s be removed",
+      folks_individual_get_alias (individual), remove_ ? "WILL" : "WILL NOT");
+
+  data = g_slice_new0 (ShowActiveData);
+
+  data->self = g_object_ref (self);
+  data->individual = g_object_ref (individual);
+  data->remove = remove_;
+
+  return data;
+}
+
+static void
+individual_store_contact_active_free (ShowActiveData *data)
+{
+  g_object_unref (data->individual);
+  g_object_unref (data->self);
+
+  g_slice_free (ShowActiveData, data);
+}
+
+static gboolean
+individual_store_contact_active_cb (ShowActiveData *data)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  priv = GET_PRIV (data->self);
+
+  if (data->remove &&
+      !priv->show_offline && !folks_individual_is_online (data->individual))
+    {
+      DEBUG ("Individual'%s' active timeout, removing item",
+          folks_individual_get_alias (data->individual));
+      individual_store_remove_individual (data->self, data->individual);
+    }
+
+  DEBUG ("Individual'%s' no longer active",
+      folks_individual_get_alias (data->individual));
+
+  individual_store_contact_set_active (data->self,
+      data->individual, FALSE, TRUE);
+
+  individual_store_contact_active_free (data);
+
+  return FALSE;
+}
+
+static void
+individual_store_contact_update (EmpathyIndividualStore *self,
+    FolksIndividual *individual)
+{
+  EmpathyIndividualStorePriv *priv;
+  ShowActiveData *data;
+  GtkTreeModel *model;
+  GList *iters, *l;
+  gboolean in_list;
+  gboolean should_be_in_list;
+  gboolean was_online = TRUE;
+  gboolean now_online = FALSE;
+  gboolean set_model = FALSE;
+  gboolean do_remove = FALSE;
+  gboolean do_set_active = FALSE;
+  gboolean do_set_refresh = FALSE;
+  gboolean show_avatar = FALSE;
+  GdkPixbuf *pixbuf_avatar;
+  GdkPixbuf *pixbuf_status;
+
+  priv = GET_PRIV (self);
+
+  model = GTK_TREE_MODEL (self);
+
+  iters = individual_store_find_contact (self, individual);
+  if (!iters)
+    {
+      in_list = FALSE;
+    }
+  else
+    {
+      in_list = TRUE;
+    }
+
+  /* Get online state now. */
+  now_online = folks_individual_is_online (individual);
+
+  if (priv->show_offline || now_online)
+    {
+      should_be_in_list = TRUE;
+    }
+  else
+    {
+      should_be_in_list = FALSE;
+    }
+
+  if (!in_list && !should_be_in_list)
+    {
+      /* Nothing to do. */
+      DEBUG ("Individual:'%s' in list:NO, should be:NO",
+          folks_individual_get_alias (individual));
+
+      g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
+      g_list_free (iters);
+      return;
+    }
+  else if (in_list && !should_be_in_list)
+    {
+      DEBUG ("Individual:'%s' in list:YES, should be:NO",
+          folks_individual_get_alias (individual));
+
+      if (priv->show_active)
+        {
+          do_remove = TRUE;
+          do_set_active = TRUE;
+          do_set_refresh = TRUE;
+
+          set_model = TRUE;
+          DEBUG ("Remove item (after timeout)");
+        }
+      else
+        {
+          DEBUG ("Remove item (now)!");
+          individual_store_remove_individual (self, individual);
+        }
+    }
+  else if (!in_list && should_be_in_list)
+    {
+      DEBUG ("Individual'%s' in list:NO, should be:YES",
+          folks_individual_get_alias (individual));
+
+      individual_store_add_individual (self, individual);
+
+      if (priv->show_active)
+        {
+          do_set_active = TRUE;
+
+          DEBUG ("Set active (individual added)");
+        }
+    }
+  else
+    {
+      DEBUG ("Individual'%s' in list:YES, should be:YES",
+          folks_individual_get_alias (individual));
+
+      /* Get online state before. */
+      if (iters && g_list_length (iters) > 0)
+        {
+          gtk_tree_model_get (model, iters->data,
+              EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &was_online, -1);
+        }
+
+      /* Is this really an update or an online/offline. */
+      if (priv->show_active)
+        {
+          if (was_online != now_online)
+            {
+              do_set_active = TRUE;
+              do_set_refresh = TRUE;
+
+              DEBUG ("Set active (individual updated %s)",
+                  was_online ? "online  -> offline" : "offline -> online");
+            }
+          else
+            {
+              /* Was TRUE for presence updates. */
+              /* do_set_active = FALSE;  */
+              do_set_refresh = TRUE;
+
+              DEBUG ("Set active (individual updated)");
+            }
+        }
+
+      set_model = TRUE;
+    }
+
+  if (priv->show_avatars && !priv->is_compact)
+    {
+      show_avatar = TRUE;
+    }
+  /* TODO: implement */
+  DEBUG ("avatars unimplemented");
+  pixbuf_status =
+      empathy_individual_store_get_individual_status_icon (self, individual);
+  pixbuf_avatar = NULL;
+
+  for (l = iters; l && set_model; l = l->next)
+    {
+      gtk_tree_store_set (GTK_TREE_STORE (self), l->data,
+          EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS, pixbuf_status,
+          EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR, pixbuf_avatar,
+          EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR_VISIBLE, show_avatar,
+          EMPATHY_INDIVIDUAL_STORE_COL_NAME,
+          folks_individual_get_alias (individual),
+          EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE,
+          folks_individual_get_presence_type (individual),
+          EMPATHY_INDIVIDUAL_STORE_COL_STATUS,
+          folks_individual_get_presence_message (individual),
+          EMPATHY_INDIVIDUAL_STORE_COL_CAN_AUDIO_CALL,
+          folks_individual_get_capabilities (individual) &
+          FOLKS_CAPABILITIES_FLAGS_AUDIO,
+          EMPATHY_INDIVIDUAL_STORE_COL_CAN_VIDEO_CALL,
+          folks_individual_get_capabilities (individual) &
+          FOLKS_CAPABILITIES_FLAGS_VIDEO,
+          EMPATHY_INDIVIDUAL_STORE_COL_COMPACT, priv->is_compact,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, FALSE,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, now_online,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, FALSE, -1);
+    }
+
+  if (pixbuf_avatar)
+    {
+      g_object_unref (pixbuf_avatar);
+    }
+
+  if (priv->show_active && do_set_active)
+    {
+      individual_store_contact_set_active (self, individual, do_set_active,
+          do_set_refresh);
+
+      if (do_set_active)
+        {
+          data =
+              individual_store_contact_active_new (self, individual,
+              do_remove);
+          g_timeout_add_seconds (ACTIVE_USER_SHOW_TIME,
+              (GSourceFunc) individual_store_contact_active_cb, data);
+        }
+    }
+
+  /* FIXME: when someone goes online then offline quickly, the
+   * first timeout sets the user to be inactive and the second
+   * timeout removes the user from the contact list, really we
+   * should remove the first timeout.
+   */
+  g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
+  g_list_free (iters);
+}
+
+static void
+individual_store_contact_updated_cb (FolksIndividual *individual,
+    GParamSpec *param,
+    EmpathyIndividualStore *self)
+{
+  DEBUG ("Individual'%s' updated, checking roster is in sync...",
+      folks_individual_get_alias (individual));
+
+  individual_store_contact_update (self, individual);
+}
+
+static void
+individual_store_add_individual_and_connect (EmpathyIndividualStore *self,
+    FolksIndividual *individual)
+{
+  g_signal_connect (individual, "notify::presence-type",
+      G_CALLBACK (individual_store_contact_updated_cb), self);
+  g_signal_connect (individual, "notify::presence-message",
+      G_CALLBACK (individual_store_contact_updated_cb), self);
+  g_signal_connect (individual, "notify::name",
+      G_CALLBACK (individual_store_contact_updated_cb), self);
+  g_signal_connect (individual, "notify::avatar",
+      G_CALLBACK (individual_store_contact_updated_cb), self);
+  g_signal_connect (individual, "notify::capabilities",
+      G_CALLBACK (individual_store_contact_updated_cb), self);
+
+  individual_store_add_individual (self, individual);
+}
+
+static void
+individual_store_remove_individual_and_disconnect (
+    EmpathyIndividualStore *self,
+    FolksIndividual *individual)
+{
+  g_signal_handlers_disconnect_by_func (individual,
+      G_CALLBACK (individual_store_contact_updated_cb), self);
+
+  individual_store_remove_individual (self, individual);
+}
+
+static void
+individual_store_members_changed_cb (EmpathyIndividualManager *manager,
+    gchar *message,
+    GList *added,
+    GList *removed,
+    guint reason,
+    EmpathyIndividualStore *self)
+{
+  GList *l;
+
+  for (l = added; l; l = l->next)
+    {
+      DEBUG ("Individual %s %s", folks_individual_get_id (l->data), "added");
+
+      individual_store_add_individual_and_connect (self, l->data);
+    }
+  for (l = removed; l; l = l->next)
+    {
+      DEBUG ("Individual %s %s",
+          folks_individual_get_id (l->data), "removed");
+
+      individual_store_remove_individual_and_disconnect (self, l->data);
+    }
+}
+
+static void
+individual_store_groups_changed_cb (EmpathyIndividualManager *manager,
+    FolksIndividual *individual,
+    gchar *group,
+    gboolean is_member,
+    EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+  gboolean show_active;
+
+  priv = GET_PRIV (self);
+
+  DEBUG ("Updating groups for individual %s",
+      folks_individual_get_id (individual));
+
+  /* We do this to make sure the groups are correct, if not, we
+   * would have to check the groups already set up for each
+   * contact and then see what has been updated.
+   */
+  show_active = priv->show_active;
+  priv->show_active = FALSE;
+  individual_store_remove_individual (self, individual);
+  individual_store_add_individual (self, individual);
+  priv->show_active = show_active;
+}
+
+static gboolean
+individual_store_manager_setup (gpointer user_data)
+{
+  EmpathyIndividualStore *self = user_data;
+  EmpathyIndividualStorePriv *priv = GET_PRIV (self);
+  GList *contacts;
+
+  /* Signal connection. */
+
+  /* TODO: implement */
+  DEBUG ("handling individual renames unimplemented");
+
+  g_signal_connect (priv->manager,
+      "members-changed",
+      G_CALLBACK (individual_store_members_changed_cb), self);
+
+  /* TODO: implement */
+  DEBUG ("handling individual favourite status changes unimplemented");
+
+  g_signal_connect (priv->manager,
+      "groups-changed",
+      G_CALLBACK (individual_store_groups_changed_cb), self);
+
+  /* Add contacts already created. */
+  contacts = empathy_individual_manager_get_members (priv->manager);
+  individual_store_members_changed_cb (priv->manager, "initial add",
+      contacts, NULL, 0, self);
+  g_list_free (contacts);
+
+  priv->setup_idle_id = 0;
+  return FALSE;
+}
+
+static void
+individual_store_set_individual_manager (EmpathyIndividualStore *self,
+    EmpathyIndividualManager *manager)
+{
+  EmpathyIndividualStorePriv *priv = GET_PRIV (self);
+
+  priv->manager = g_object_ref (manager);
+
+  /* Let a chance to have all properties set before populating */
+  priv->setup_idle_id = g_idle_add (individual_store_manager_setup, self);
+}
+
+static void
+individual_store_member_renamed_cb (EmpathyIndividualManager *manager,
+    FolksIndividual *old_individual,
+    FolksIndividual *new_individual,
+    guint reason,
+    gchar *message,
+    EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  priv = GET_PRIV (self);
+
+  DEBUG ("Individual %s renamed to %s",
+      folks_individual_get_id (old_individual),
+      folks_individual_get_id (new_individual));
+
+  /* add the new contact */
+  individual_store_add_individual_and_connect (self, new_individual);
+
+  /* remove old contact */
+  individual_store_remove_individual_and_disconnect (self, old_individual);
+}
+
+static void
+individual_store_favourites_changed_cb (EmpathyIndividualManager *manager,
+    FolksIndividual *individual,
+    gboolean is_favourite,
+    EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  priv = GET_PRIV (self);
+
+  DEBUG ("Individual %s is %s a favourite",
+      folks_individual_get_id (individual),
+      is_favourite ? "now" : "no longer");
+
+  individual_store_remove_individual (self, individual);
+  individual_store_add_individual (self, individual);
+}
+
+static void
+individual_store_dispose (GObject *object)
+{
+  EmpathyIndividualStorePriv *priv = GET_PRIV (object);
+  GList *contacts, *l;
+
+  if (priv->dispose_has_run)
+    return;
+  priv->dispose_has_run = TRUE;
+
+  contacts = empathy_individual_manager_get_members (priv->manager);
+  for (l = contacts; l; l = l->next)
+    {
+      g_signal_handlers_disconnect_by_func (l->data,
+          G_CALLBACK (individual_store_contact_updated_cb), object);
+
+      g_object_unref (l->data);
+    }
+  g_list_free (contacts);
+
+  g_signal_handlers_disconnect_by_func (priv->manager,
+      G_CALLBACK (individual_store_member_renamed_cb), object);
+  g_signal_handlers_disconnect_by_func (priv->manager,
+      G_CALLBACK (individual_store_members_changed_cb), object);
+  g_signal_handlers_disconnect_by_func (priv->manager,
+      G_CALLBACK (individual_store_favourites_changed_cb), object);
+  g_signal_handlers_disconnect_by_func (priv->manager,
+      G_CALLBACK (individual_store_groups_changed_cb), object);
+  g_object_unref (priv->manager);
+
+  if (priv->inhibit_active)
+    {
+      g_source_remove (priv->inhibit_active);
+    }
+
+  if (priv->setup_idle_id != 0)
+    {
+      g_source_remove (priv->setup_idle_id);
+    }
+
+  g_hash_table_destroy (priv->status_icons);
+  G_OBJECT_CLASS (empathy_individual_store_parent_class)->dispose (object);
+}
+
+static void
+individual_store_get_property (GObject *object,
+    guint param_id,
+    GValue *value,
+    GParamSpec *pspec)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  priv = GET_PRIV (object);
+
+  switch (param_id)
+    {
+    case PROP_INDIVIDUAL_MANAGER:
+      g_value_set_object (value, priv->manager);
+      break;
+    case PROP_SHOW_OFFLINE:
+      g_value_set_boolean (value, priv->show_offline);
+      break;
+    case PROP_SHOW_AVATARS:
+      g_value_set_boolean (value, priv->show_avatars);
+      break;
+    case PROP_SHOW_PROTOCOLS:
+      g_value_set_boolean (value, priv->show_protocols);
+      break;
+    case PROP_SHOW_GROUPS:
+      g_value_set_boolean (value, priv->show_groups);
+      break;
+    case PROP_IS_COMPACT:
+      g_value_set_boolean (value, priv->is_compact);
+      break;
+    case PROP_SORT_CRITERIUM:
+      g_value_set_enum (value, priv->sort_criterium);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+      break;
+    };
+}
+
+static void
+individual_store_set_property (GObject *object,
+    guint param_id,
+    const GValue *value,
+    GParamSpec *pspec)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  priv = GET_PRIV (object);
+
+  switch (param_id)
+    {
+    case PROP_INDIVIDUAL_MANAGER:
+      individual_store_set_individual_manager (EMPATHY_INDIVIDUAL_STORE
+          (object), g_value_get_object (value));
+      break;
+    case PROP_SHOW_OFFLINE:
+      empathy_individual_store_set_show_offline (EMPATHY_INDIVIDUAL_STORE
+          (object), g_value_get_boolean (value));
+      break;
+    case PROP_SHOW_AVATARS:
+      empathy_individual_store_set_show_avatars (EMPATHY_INDIVIDUAL_STORE
+          (object), g_value_get_boolean (value));
+      break;
+    case PROP_SHOW_PROTOCOLS:
+      empathy_individual_store_set_show_protocols (EMPATHY_INDIVIDUAL_STORE
+          (object), g_value_get_boolean (value));
+      break;
+    case PROP_SHOW_GROUPS:
+      empathy_individual_store_set_show_groups (EMPATHY_INDIVIDUAL_STORE
+          (object), g_value_get_boolean (value));
+      break;
+    case PROP_IS_COMPACT:
+      empathy_individual_store_set_is_compact (EMPATHY_INDIVIDUAL_STORE
+          (object), g_value_get_boolean (value));
+      break;
+    case PROP_SORT_CRITERIUM:
+      empathy_individual_store_set_sort_criterium (EMPATHY_INDIVIDUAL_STORE
+          (object), g_value_get_enum (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+      break;
+    };
+}
+
+static void
+empathy_individual_store_class_init (EmpathyIndividualStoreClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = individual_store_dispose;
+  object_class->get_property = individual_store_get_property;
+  object_class->set_property = individual_store_set_property;
+
+  g_object_class_install_property (object_class,
+      PROP_INDIVIDUAL_MANAGER,
+      g_param_spec_object ("individual-manager",
+          "The individual manager",
+          "The individual manager",
+          EMPATHY_TYPE_INDIVIDUAL_MANAGER,
+          G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_SHOW_OFFLINE,
+      g_param_spec_boolean ("show-offline",
+          "Show Offline",
+          "Whether contact list should display "
+          "offline contacts", FALSE, G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_SHOW_AVATARS,
+      g_param_spec_boolean ("show-avatars",
+          "Show Avatars",
+          "Whether contact list should display "
+          "avatars for contacts", TRUE, G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_SHOW_PROTOCOLS,
+      g_param_spec_boolean ("show-protocols",
+          "Show Protocols",
+          "Whether contact list should display "
+          "protocols for contacts", FALSE, G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_SHOW_GROUPS,
+      g_param_spec_boolean ("show-groups",
+          "Show Groups",
+          "Whether contact list should display "
+          "contact groups", TRUE, G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_IS_COMPACT,
+      g_param_spec_boolean ("is-compact",
+          "Is Compact",
+          "Whether the contact list is in compact mode or not",
+          FALSE, G_PARAM_READWRITE));
+
+  g_object_class_install_property (object_class,
+      PROP_SORT_CRITERIUM,
+      g_param_spec_enum ("sort-criterium",
+          "Sort citerium",
+          "The sort criterium to use for sorting the contact list",
+          EMPATHY_TYPE_INDIVIDUAL_STORE_SORT,
+          EMPATHY_INDIVIDUAL_STORE_SORT_NAME, G_PARAM_READWRITE));
+
+  g_type_class_add_private (object_class,
+      sizeof (EmpathyIndividualStorePriv));
+}
+
+static gint
+get_position (const char **strv,
+    const char *str)
+{
+  int i;
+
+  for (i = 0; strv[i] != NULL; i++)
+    {
+      if (!tp_strdiff (strv[i], str))
+        return i;
+    }
+
+  return -1;
+}
+
+static gint
+compare_separator_and_groups (gboolean is_separator_a,
+    gboolean is_separator_b,
+    const gchar *name_a,
+    const gchar *name_b,
+    FolksIndividual *individual_a,
+    FolksIndividual *individual_b,
+    gboolean fake_group_a,
+    gboolean fake_group_b)
+{
+  /* these two lists are the sorted list of fake groups to include at the
+   * top and bottom of the roster */
+  const char *top_groups[] = {
+    EMPATHY_INDIVIDUAL_STORE_FAVORITE,
+    NULL
+  };
+
+  const char *bottom_groups[] = {
+    EMPATHY_INDIVIDUAL_STORE_UNGROUPED,
+    NULL
+  };
+
+  if (is_separator_a || is_separator_b)
+    {
+      /* We have at least one separator */
+      if (is_separator_a)
+        {
+          return -1;
+        }
+      else if (is_separator_b)
+        {
+          return 1;
+        }
+    }
+
+  /* One group and one contact */
+  if (!individual_a && individual_b)
+    {
+      return 1;
+    }
+  else if (individual_a && !individual_b)
+    {
+      return -1;
+    }
+  else if (!individual_a && !individual_b)
+    {
+      gboolean a_in_top, b_in_top, a_in_bottom, b_in_bottom;
+
+      a_in_top = fake_group_a && tp_strv_contains (top_groups, name_a);
+      b_in_top = fake_group_b && tp_strv_contains (top_groups, name_b);
+      a_in_bottom = fake_group_a && tp_strv_contains (bottom_groups, name_a);
+      b_in_bottom = fake_group_b && tp_strv_contains (bottom_groups, name_b);
+
+      if (a_in_top && b_in_top)
+        {
+          /* compare positions */
+          return CLAMP (get_position (top_groups, name_a) -
+              get_position (top_groups, name_b), -1, 1);
+        }
+      else if (a_in_bottom && b_in_bottom)
+        {
+          /* compare positions */
+          return CLAMP (get_position (bottom_groups, name_a) -
+              get_position (bottom_groups, name_b), -1, 1);
+        }
+      else if (a_in_top || b_in_bottom)
+        {
+          return -1;
+        }
+      else if (b_in_top || a_in_bottom)
+        {
+          return 1;
+        }
+      else
+        {
+          return g_utf8_collate (name_a, name_b);
+        }
+    }
+
+  /* Two contacts, ordering depends of the sorting policy */
+  return 0;
+}
+
+static gint
+individual_store_contact_sort (FolksIndividual *individual_a,
+    FolksIndividual *individual_b)
+{
+  gint ret_val;
+
+  /* alias */
+  ret_val = g_utf8_collate (folks_individual_get_alias (individual_a),
+      folks_individual_get_alias (individual_b));
+
+  if (ret_val != 0)
+    goto out;
+
+  /* identifier */
+  ret_val = g_utf8_collate (folks_individual_get_id (individual_a),
+      folks_individual_get_id (individual_b));
+
+  if (ret_val != 0)
+    goto out;
+
+out:
+  return ret_val;
+}
+
+static gint
+individual_store_state_sort_func (GtkTreeModel *model,
+    GtkTreeIter *iter_a,
+    GtkTreeIter *iter_b,
+    gpointer user_data)
+{
+  gint ret_val;
+  FolksIndividual *individual_a, *individual_b;
+  gchar *name_a, *name_b;
+  gboolean is_separator_a, is_separator_b;
+  gboolean fake_group_a, fake_group_b;
+  FolksPresenceType folks_presence_type_a, folks_presence_type_b;
+  TpConnectionPresenceType tp_presence_a, tp_presence_b;
+
+  gtk_tree_model_get (model, iter_a,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name_a,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual_a,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator_a,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake_group_a, -1);
+  gtk_tree_model_get (model, iter_b,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name_b,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual_b,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator_b,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake_group_b, -1);
+
+  ret_val = compare_separator_and_groups (is_separator_a, is_separator_b,
+      name_a, name_b, individual_a, individual_b, fake_group_a, fake_group_b);
+
+  if (ret_val != 0)
+    {
+      goto free_and_out;
+    }
+
+  /* If we managed to get this far, we can start looking at
+   * the presences.
+   */
+  folks_presence_type_a = folks_individual_get_presence_type (individual_a);
+  folks_presence_type_b = folks_individual_get_presence_type (individual_b);
+  tp_presence_a = empathy_folks_presence_type_to_tp (folks_presence_type_a);
+  tp_presence_b = empathy_folks_presence_type_to_tp (folks_presence_type_b);
+
+  ret_val = -tp_connection_presence_type_cmp_availability (tp_presence_a,
+      tp_presence_b);
+
+  if (ret_val == 0)
+    {
+      /* Fallback: compare by name et al. */
+      ret_val = individual_store_contact_sort (individual_a, individual_b);
+    }
+
+free_and_out:
+  g_free (name_a);
+  g_free (name_b);
+
+  if (individual_a)
+    {
+      g_object_unref (individual_a);
+    }
+
+  if (individual_b)
+    {
+      g_object_unref (individual_b);
+    }
+
+  return ret_val;
+}
+
+static gint
+individual_store_name_sort_func (GtkTreeModel *model,
+    GtkTreeIter *iter_a,
+    GtkTreeIter *iter_b,
+    gpointer user_data)
+{
+  gchar *name_a, *name_b;
+  FolksIndividual *individual_a, *individual_b;
+  gboolean is_separator_a = FALSE, is_separator_b = FALSE;
+  gint ret_val;
+  gboolean fake_group_a, fake_group_b;
+
+  gtk_tree_model_get (model, iter_a,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name_a,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual_a,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator_a,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake_group_a, -1);
+  gtk_tree_model_get (model, iter_b,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name_b,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual_b,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator_b,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake_group_b, -1);
+
+  ret_val = compare_separator_and_groups (is_separator_a, is_separator_b,
+      name_a, name_b, individual_a, individual_b, fake_group_a, fake_group_b);
+
+  if (ret_val == 0)
+    ret_val = individual_store_contact_sort (individual_a, individual_b);
+
+  if (individual_a)
+    {
+      g_object_unref (individual_a);
+    }
+
+  if (individual_b)
+    {
+      g_object_unref (individual_b);
+    }
+
+  return ret_val;
+}
+
+static void
+individual_store_setup (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+  GType types[] = {
+    GDK_TYPE_PIXBUF,            /* Status pixbuf */
+    GDK_TYPE_PIXBUF,            /* Avatar pixbuf */
+    G_TYPE_BOOLEAN,             /* Avatar pixbuf visible */
+    G_TYPE_STRING,              /* Name */
+    G_TYPE_UINT,                /* Presence type */
+    G_TYPE_STRING,              /* Status string */
+    G_TYPE_BOOLEAN,             /* Compact view */
+    FOLKS_TYPE_INDIVIDUAL,      /* Individual type */
+    G_TYPE_BOOLEAN,             /* Is group */
+    G_TYPE_BOOLEAN,             /* Is active */
+    G_TYPE_BOOLEAN,             /* Is online */
+    G_TYPE_BOOLEAN,             /* Is separator */
+    G_TYPE_BOOLEAN,             /* Can make audio calls */
+    G_TYPE_BOOLEAN,             /* Can make video calls */
+    EMPATHY_TYPE_INDIVIDUAL_MANAGER_FLAGS,      /* Flags */
+    G_TYPE_BOOLEAN,             /* Is a fake group */
+  };
+
+  priv = GET_PRIV (self);
+
+  gtk_tree_store_set_column_types (GTK_TREE_STORE (self),
+      EMPATHY_INDIVIDUAL_STORE_COL_COUNT, types);
+
+  /* Set up sorting */
+  gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (self),
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME,
+      individual_store_name_sort_func, self, NULL);
+  gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (self),
+      EMPATHY_INDIVIDUAL_STORE_COL_STATUS,
+      individual_store_state_sort_func, self, NULL);
+
+  priv->sort_criterium = EMPATHY_INDIVIDUAL_STORE_SORT_NAME;
+  empathy_individual_store_set_sort_criterium (self, priv->sort_criterium);
+}
+
+static gboolean
+individual_store_inibit_active_cb (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  priv = GET_PRIV (self);
+
+  priv->show_active = TRUE;
+  priv->inhibit_active = 0;
+
+  return FALSE;
+}
+
+static void
+empathy_individual_store_init (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      EMPATHY_TYPE_INDIVIDUAL_STORE, EmpathyIndividualStorePriv);
+
+  self->priv = priv;
+  priv->show_avatars = TRUE;
+  priv->show_groups = TRUE;
+  priv->show_protocols = FALSE;
+  priv->inhibit_active =
+      g_timeout_add_seconds (ACTIVE_USER_WAIT_TO_ENABLE_TIME,
+      (GSourceFunc) individual_store_inibit_active_cb, self);
+  priv->status_icons =
+      g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+  individual_store_setup (self);
+}
+
+EmpathyIndividualStore *
+empathy_individual_store_new (EmpathyIndividualManager *manager)
+{
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager), NULL);
+
+  return g_object_new (EMPATHY_TYPE_INDIVIDUAL_STORE,
+      "individual-manager", manager, NULL);
+}
+
+EmpathyIndividualManager *
+empathy_individual_store_get_manager (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self), FALSE);
+
+  priv = GET_PRIV (self);
+
+  return priv->manager;
+}
+
+gboolean
+empathy_individual_store_get_show_offline (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self), FALSE);
+
+  priv = GET_PRIV (self);
+
+  return priv->show_offline;
+}
+
+void
+empathy_individual_store_set_show_offline (EmpathyIndividualStore *self,
+    gboolean show_offline)
+{
+  EmpathyIndividualStorePriv *priv;
+  GList *contacts, *l;
+  gboolean show_active;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self));
+
+  priv = GET_PRIV (self);
+
+  priv->show_offline = show_offline;
+  show_active = priv->show_active;
+
+  /* Disable temporarily. */
+  priv->show_active = FALSE;
+
+  contacts = empathy_individual_manager_get_members (priv->manager);
+  for (l = contacts; l; l = l->next)
+    {
+      individual_store_contact_update (self, l->data);
+
+      g_object_unref (l->data);
+    }
+  g_list_free (contacts);
+
+  /* Restore to original setting. */
+  priv->show_active = show_active;
+
+  g_object_notify (G_OBJECT (self), "show-offline");
+}
+
+gboolean
+empathy_individual_store_get_show_avatars (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self), TRUE);
+
+  priv = GET_PRIV (self);
+
+  return priv->show_avatars;
+}
+
+static gboolean
+individual_store_update_list_mode_foreach (GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkTreeIter *iter,
+    EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+  gboolean show_avatar = FALSE;
+  FolksIndividual *individual;
+  GdkPixbuf *pixbuf_status;
+
+  priv = GET_PRIV (self);
+
+  if (priv->show_avatars && !priv->is_compact)
+    {
+      show_avatar = TRUE;
+    }
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
+
+  if (individual == NULL)
+    {
+      return FALSE;
+    }
+  /* get icon from hash_table */
+  pixbuf_status =
+      empathy_individual_store_get_individual_status_icon (self, individual);
+
+  gtk_tree_store_set (GTK_TREE_STORE (self), iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS, pixbuf_status,
+      EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR_VISIBLE, show_avatar,
+      EMPATHY_INDIVIDUAL_STORE_COL_COMPACT, priv->is_compact, -1);
+
+  return FALSE;
+}
+
+void
+empathy_individual_store_set_show_avatars (EmpathyIndividualStore *self,
+    gboolean show_avatars)
+{
+  EmpathyIndividualStorePriv *priv;
+  GtkTreeModel *model;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self));
+
+  priv = GET_PRIV (self);
+
+  priv->show_avatars = show_avatars;
+
+  model = GTK_TREE_MODEL (self);
+
+  gtk_tree_model_foreach (model,
+      (GtkTreeModelForeachFunc)
+      individual_store_update_list_mode_foreach, self);
+
+  g_object_notify (G_OBJECT (self), "show-avatars");
+}
+
+gboolean
+empathy_individual_store_get_show_protocols (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self), TRUE);
+
+  priv = GET_PRIV (self);
+
+  return priv->show_protocols;
+}
+
+void
+empathy_individual_store_set_show_protocols (EmpathyIndividualStore *self,
+    gboolean show_protocols)
+{
+  EmpathyIndividualStorePriv *priv;
+  GtkTreeModel *model;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self));
+
+  priv = GET_PRIV (self);
+
+  priv->show_protocols = show_protocols;
+
+  model = GTK_TREE_MODEL (self);
+
+  gtk_tree_model_foreach (model,
+      (GtkTreeModelForeachFunc)
+      individual_store_update_list_mode_foreach, self);
+
+  g_object_notify (G_OBJECT (self), "show-protocols");
+}
+
+gboolean
+empathy_individual_store_get_show_groups (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self), TRUE);
+
+  priv = GET_PRIV (self);
+
+  return priv->show_groups;
+}
+
+void
+empathy_individual_store_set_show_groups (EmpathyIndividualStore *self,
+    gboolean show_groups)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self));
+
+  priv = GET_PRIV (self);
+
+  if (priv->show_groups == show_groups)
+    {
+      return;
+    }
+
+  priv->show_groups = show_groups;
+
+  if (priv->setup_idle_id == 0)
+    {
+      /* Remove all contacts and add them back, not optimized but
+       * that's the easy way :)
+       *
+       * This is only done if there's not a pending setup idle
+       * callback, otherwise it will race and the contacts will get
+       * added twice */
+      GList *contacts;
+
+      gtk_tree_store_clear (GTK_TREE_STORE (self));
+      contacts = empathy_individual_manager_get_members (priv->manager);
+
+      individual_store_members_changed_cb (priv->manager,
+          "re-adding members: toggled group visibility",
+          contacts, NULL, 0, self);
+      g_list_foreach (contacts, (GFunc) g_free, NULL);
+      g_list_free (contacts);
+    }
+
+  g_object_notify (G_OBJECT (self), "show-groups");
+}
+
+gboolean
+empathy_individual_store_get_is_compact (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self), TRUE);
+
+  priv = GET_PRIV (self);
+
+  return priv->is_compact;
+}
+
+void
+empathy_individual_store_set_is_compact (EmpathyIndividualStore *self,
+    gboolean is_compact)
+{
+  EmpathyIndividualStorePriv *priv;
+  GtkTreeModel *model;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self));
+
+  priv = GET_PRIV (self);
+
+  priv->is_compact = is_compact;
+
+  model = GTK_TREE_MODEL (self);
+
+  gtk_tree_model_foreach (model,
+      (GtkTreeModelForeachFunc)
+      individual_store_update_list_mode_foreach, self);
+
+  g_object_notify (G_OBJECT (self), "is-compact");
+}
+
+EmpathyIndividualStoreSort
+empathy_individual_store_get_sort_criterium (EmpathyIndividualStore *self)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self), 0);
+
+  priv = GET_PRIV (self);
+
+  return priv->sort_criterium;
+}
+
+void
+empathy_individual_store_set_sort_criterium (EmpathyIndividualStore *self,
+    EmpathyIndividualStoreSort sort_criterium)
+{
+  EmpathyIndividualStorePriv *priv;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (self));
+
+  priv = GET_PRIV (self);
+
+  priv->sort_criterium = sort_criterium;
+
+  switch (sort_criterium)
+    {
+    case EMPATHY_INDIVIDUAL_STORE_SORT_STATE:
+      gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (self),
+          EMPATHY_INDIVIDUAL_STORE_COL_STATUS, GTK_SORT_ASCENDING);
+      break;
+
+    case EMPATHY_INDIVIDUAL_STORE_SORT_NAME:
+      gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (self),
+          EMPATHY_INDIVIDUAL_STORE_COL_NAME, GTK_SORT_ASCENDING);
+      break;
+    }
+
+  g_object_notify (G_OBJECT (self), "sort-criterium");
+}
+
+gboolean
+empathy_individual_store_row_separator_func (GtkTreeModel *model,
+    GtkTreeIter *iter,
+    gpointer data)
+{
+  gboolean is_separator = FALSE;
+
+  g_return_val_if_fail (GTK_IS_TREE_MODEL (model), FALSE);
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator, -1);
+
+  return is_separator;
+}
+
+gchar *
+empathy_individual_store_get_parent_group (GtkTreeModel *model,
+    GtkTreePath *path,
+    gboolean *path_is_group,
+    gboolean *is_fake_group)
+{
+  GtkTreeIter parent_iter, iter;
+  gchar *name = NULL;
+  gboolean is_group;
+  gboolean fake;
+
+  g_return_val_if_fail (GTK_IS_TREE_MODEL (model), NULL);
+
+  if (path_is_group)
+    {
+      *path_is_group = FALSE;
+    }
+
+  if (!gtk_tree_model_get_iter (model, &iter, path))
+    {
+      return NULL;
+    }
+
+  gtk_tree_model_get (model, &iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name, -1);
+
+  if (!is_group)
+    {
+      g_free (name);
+      name = NULL;
+
+      if (!gtk_tree_model_iter_parent (model, &parent_iter, &iter))
+        {
+          return NULL;
+        }
+
+      iter = parent_iter;
+
+      gtk_tree_model_get (model, &iter,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+          EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake, -1);
+      if (!is_group)
+        {
+          g_free (name);
+          return NULL;
+        }
+    }
+
+  if (path_is_group)
+    {
+      *path_is_group = TRUE;
+    }
+
+  if (is_fake_group != NULL)
+    *is_fake_group = fake;
+
+  return name;
+}
+
+static GdkPixbuf *
+individual_store_get_individual_status_icon_with_icon_name (
+    EmpathyIndividualStore *self,
+    FolksIndividual *individual,
+    const gchar *status_icon_name)
+{
+  GdkPixbuf *pixbuf_status = NULL;
+  EmpathyIndividualStorePriv *priv;
+  const gchar *protocol_name = NULL;
+  gchar *icon_name = NULL;
+  GList *personas, *l;
+  guint contact_count;
+  EmpathyContact *contact = NULL;
+  gboolean show_protocols_here;
+
+  priv = GET_PRIV (self);
+
+  personas = folks_individual_get_personas (individual);
+  for (l = personas, contact_count = 0; l; l = l->next)
+    {
+      if (TPF_IS_PERSONA (l->data))
+        contact_count++;
+
+      if (contact_count > 1)
+        break;
+    }
+
+  show_protocols_here = priv->show_protocols && (contact_count == 1);
+  if (show_protocols_here)
+    {
+      contact = empathy_contact_from_folks_individual (individual);
+      protocol_name = empathy_protocol_name_for_contact (contact);
+      icon_name = g_strdup_printf ("%s-%s", status_icon_name, protocol_name);
+    }
+  else
+    {
+      icon_name = g_strdup_printf ("%s", status_icon_name);
+    }
+  if (pixbuf_status == NULL)
+    {
+      pixbuf_status =
+          empathy_pixbuf_contact_status_icon_with_icon_name (contact,
+          status_icon_name, priv->show_protocols);
+      if (pixbuf_status != NULL)
+        {
+          g_hash_table_insert (priv->status_icons,
+              g_strdup (icon_name), pixbuf_status);
+        }
+    }
+
+  g_free (icon_name);
+  return pixbuf_status;
+}
+
+GdkPixbuf *
+empathy_individual_store_get_individual_status_icon (
+    EmpathyIndividualStore *self,
+    FolksIndividual *individual)
+{
+  GdkPixbuf *pixbuf_status = NULL;
+  const gchar *status_icon_name = NULL;
+
+  status_icon_name = empathy_icon_name_for_individual (individual);
+  if (status_icon_name == NULL)
+    return NULL;
+
+  pixbuf_status =
+      individual_store_get_individual_status_icon_with_icon_name (self,
+      individual, status_icon_name);
+
+  return pixbuf_status;
+}
diff --git a/libempathy-gtk/empathy-individual-store.h b/libempathy-gtk/empathy-individual-store.h
new file mode 100644 (file)
index 0000000..9c85582
--- /dev/null
@@ -0,0 +1,148 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2005-2007 Imendio AB
+ * Copyright (C) 2007-2010 Collabora Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Authors: Mikael Hallendal <micke@imendio.com>
+ *          Martyn Russell <martyn@imendio.com>
+ *          Xavier Claessens <xclaesse@gmail.com>
+ *          Travis Reitter <travis.reitter@collabora.co.uk>
+ */
+
+#ifndef __EMPATHY_INDIVIDUAL_STORE_H__
+#define __EMPATHY_INDIVIDUAL_STORE_H__
+
+#include <gtk/gtk.h>
+
+#include <libempathy/empathy-individual-manager.h>
+
+G_BEGIN_DECLS
+#define EMPATHY_TYPE_INDIVIDUAL_STORE         (empathy_individual_store_get_type ())
+#define EMPATHY_INDIVIDUAL_STORE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), EMPATHY_TYPE_INDIVIDUAL_STORE, EmpathyIndividualStore))
+#define EMPATHY_INDIVIDUAL_STORE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), EMPATHY_TYPE_INDIVIDUAL_STORE, EmpathyIndividualStoreClass))
+#define EMPATHY_IS_INDIVIDUAL_STORE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), EMPATHY_TYPE_INDIVIDUAL_STORE))
+#define EMPATHY_IS_INDIVIDUAL_STORE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), EMPATHY_TYPE_INDIVIDUAL_STORE))
+#define EMPATHY_INDIVIDUAL_STORE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), EMPATHY_TYPE_INDIVIDUAL_STORE, EmpathyIndividualStoreClass))
+typedef struct _EmpathyIndividualStore EmpathyIndividualStore;
+typedef struct _EmpathyIndividualStoreClass EmpathyIndividualStoreClass;
+
+typedef enum
+{
+  EMPATHY_INDIVIDUAL_STORE_SORT_STATE,
+  EMPATHY_INDIVIDUAL_STORE_SORT_NAME
+} EmpathyIndividualStoreSort;
+
+typedef enum
+{
+  EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS,
+  EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR,
+  EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR_VISIBLE,
+  EMPATHY_INDIVIDUAL_STORE_COL_NAME,
+  EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE,
+  EMPATHY_INDIVIDUAL_STORE_COL_STATUS,
+  EMPATHY_INDIVIDUAL_STORE_COL_COMPACT,
+  EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL,
+  EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP,
+  EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE,
+  EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE,
+  EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR,
+  EMPATHY_INDIVIDUAL_STORE_COL_CAN_AUDIO_CALL,
+  EMPATHY_INDIVIDUAL_STORE_COL_CAN_VIDEO_CALL,
+  EMPATHY_INDIVIDUAL_STORE_COL_FLAGS,
+  EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP,
+  EMPATHY_INDIVIDUAL_STORE_COL_COUNT,
+} EmpathyIndividualStoreCol;
+
+#define EMPATHY_INDIVIDUAL_STORE_UNGROUPED _("Ungrouped")
+#define EMPATHY_INDIVIDUAL_STORE_FAVORITE  _("Favorite People")
+#define EMPATHY_INDIVIDUAL_STORE_PEOPLE_NEARBY _("People Nearby")
+
+struct _EmpathyIndividualStore
+{
+  GtkTreeStore parent;
+  gpointer priv;
+};
+
+struct _EmpathyIndividualStoreClass
+{
+  GtkTreeStoreClass parent_class;
+};
+
+GType
+empathy_individual_store_get_type (void) G_GNUC_CONST;
+
+EmpathyIndividualStore *empathy_individual_store_new (
+    EmpathyIndividualManager *manager);
+
+EmpathyIndividualManager *empathy_individual_store_get_manager (
+    EmpathyIndividualStore *store);
+
+gboolean empathy_individual_store_get_show_offline (
+    EmpathyIndividualStore *store);
+
+void empathy_individual_store_set_show_offline (
+    EmpathyIndividualStore *store,
+    gboolean show_offline);
+
+gboolean empathy_individual_store_get_show_avatars (
+    EmpathyIndividualStore *store);
+
+void empathy_individual_store_set_show_avatars (EmpathyIndividualStore *store,
+    gboolean show_avatars);
+
+gboolean empathy_individual_store_get_show_groups (
+    EmpathyIndividualStore *store);
+
+void empathy_individual_store_set_show_groups (EmpathyIndividualStore *store,
+    gboolean show_groups);
+
+gboolean empathy_individual_store_get_is_compact (
+    EmpathyIndividualStore *store);
+
+void empathy_individual_store_set_is_compact (EmpathyIndividualStore *store,
+    gboolean is_compact);
+
+gboolean empathy_individual_store_get_show_protocols (
+    EmpathyIndividualStore *store);
+
+void empathy_individual_store_set_show_protocols (
+    EmpathyIndividualStore *store,
+    gboolean show_protocols);
+
+EmpathyIndividualStoreSort empathy_individual_store_get_sort_criterium (
+    EmpathyIndividualStore *store);
+
+void empathy_individual_store_set_sort_criterium (
+    EmpathyIndividualStore *store,
+    EmpathyIndividualStoreSort sort_criterium);
+
+gboolean empathy_individual_store_row_separator_func (GtkTreeModel *model,
+    GtkTreeIter *iter,
+    gpointer data);
+
+gchar *empathy_individual_store_get_parent_group (GtkTreeModel *model,
+    GtkTreePath *path,
+    gboolean *path_is_group,
+    gboolean *is_fake_group);
+
+GdkPixbuf *empathy_individual_store_get_individual_status_icon (
+    EmpathyIndividualStore *store,
+    FolksIndividual *individual);
+
+G_END_DECLS
+#endif /* __EMPATHY_INDIVIDUAL_STORE_H__ */
diff --git a/libempathy-gtk/empathy-individual-view.c b/libempathy-gtk/empathy-individual-view.c
new file mode 100644 (file)
index 0000000..8fd2d63
--- /dev/null
@@ -0,0 +1,2125 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2005-2007 Imendio AB
+ * Copyright (C) 2007-2010 Collabora Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Authors: Mikael Hallendal <micke@imendio.com>
+ *          Martyn Russell <martyn@imendio.com>
+ *          Xavier Claessens <xclaesse@gmail.com>
+ *          Travis Reitter <travis.reitter@collabora.co.uk>
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <glib/gi18n-lib.h>
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+
+#include <folks/folks.h>
+#include <telepathy-glib/account-manager.h>
+#include <telepathy-glib/util.h>
+
+#include <libempathy/empathy-call-factory.h>
+#include <libempathy/empathy-individual-manager.h>
+#include <libempathy/empathy-contact-groups.h>
+#include <libempathy/empathy-dispatcher.h>
+#include <libempathy/empathy-utils.h>
+
+#include "empathy-individual-view.h"
+#include "empathy-individual-menu.h"
+#include "empathy-individual-store.h"
+#include "empathy-images.h"
+#include "empathy-cell-renderer-expander.h"
+#include "empathy-cell-renderer-text.h"
+#include "empathy-cell-renderer-activatable.h"
+#include "empathy-ui-utils.h"
+#include "empathy-gtk-enum-types.h"
+#include "empathy-gtk-marshal.h"
+
+#define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
+#include <libempathy/empathy-debug.h>
+
+/* Active users are those which have recently changed state
+ * (e.g. online, offline or from normal to a busy state).
+ */
+
+#define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyIndividualView)
+typedef struct
+{
+  EmpathyIndividualStore *store;
+  GtkTreeRowReference *drag_row;
+  EmpathyIndividualViewFeatureFlags view_features;
+  EmpathyContactFeatureFlags individual_features;
+  GtkWidget *tooltip_widget;
+  GtkTargetList *file_targets;
+
+  GtkTreeModelFilter *filter;
+  GtkWidget *search_widget;
+} EmpathyIndividualViewPriv;
+
+typedef struct
+{
+  EmpathyIndividualView *view;
+  GtkTreePath *path;
+  guint timeout_id;
+} DragMotionData;
+
+typedef struct
+{
+  EmpathyIndividualView *view;
+  FolksIndividual *individual;
+  gboolean remove;
+} ShowActiveData;
+
+enum
+{
+  PROP_0,
+  PROP_STORE,
+  PROP_VIEW_FEATURES,
+  PROP_INDIVIDUAL_FEATURES,
+};
+
+/* TODO: re-add DRAG_TYPE_CONTACT_ID, for the case that we're dragging around
+ * specific EmpathyContacts (between/in/out of Individuals) */
+enum DndDragType
+{
+  DND_DRAG_TYPE_INDIVIDUAL_ID,
+  DND_DRAG_TYPE_URI_LIST,
+  DND_DRAG_TYPE_STRING,
+};
+
+static const GtkTargetEntry drag_types_dest[] = {
+  {"text/path-list", 0, DND_DRAG_TYPE_URI_LIST},
+  {"text/uri-list", 0, DND_DRAG_TYPE_URI_LIST},
+  {"text/contact-id", 0, DND_DRAG_TYPE_INDIVIDUAL_ID},
+  {"text/plain", 0, DND_DRAG_TYPE_STRING},
+  {"STRING", 0, DND_DRAG_TYPE_STRING},
+};
+
+static const GtkTargetEntry drag_types_dest_file[] = {
+  {"text/path-list", 0, DND_DRAG_TYPE_URI_LIST},
+  {"text/uri-list", 0, DND_DRAG_TYPE_URI_LIST},
+};
+
+static const GtkTargetEntry drag_types_source[] = {
+  {"text/contact-id", 0, DND_DRAG_TYPE_INDIVIDUAL_ID},
+};
+
+static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)];
+static GdkAtom drag_atoms_source[G_N_ELEMENTS (drag_types_source)];
+
+enum
+{
+  DRAG_CONTACT_RECEIVED,
+  LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE (EmpathyIndividualView, empathy_individual_view,
+    GTK_TYPE_TREE_VIEW);
+
+static gboolean
+individual_view_is_visible_individual (EmpathyIndividualView *self,
+    FolksIndividual *individual)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (self);
+  EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
+  const gchar *str;
+  const gchar *p;
+  gchar *dup_str = NULL;
+  gboolean visible;
+  GList *personas, *l;
+
+  g_assert (live != NULL);
+
+  /* check alias name */
+  str = folks_individual_get_alias (individual);
+  if (empathy_live_search_match (live, str))
+    return TRUE;
+
+  /* check contact id, remove the @server.com part */
+  personas = folks_individual_get_personas (individual);
+  for (l = personas; l; l = l->next)
+    {
+      str = folks_persona_get_uid (l->data);
+      p = strstr (str, "@");
+      if (p != NULL)
+        str = dup_str = g_strndup (str, p - str);
+
+      visible = empathy_live_search_match (live, str);
+      g_free (dup_str);
+      if (visible)
+        return TRUE;
+    }
+
+  /* FIXME: Add more rules here, we could check phone numbers in
+   * contact's vCard for example. */
+
+  return FALSE;
+}
+
+static gboolean
+individual_view_filter_visible_func (GtkTreeModel *model,
+    GtkTreeIter *iter,
+    gpointer user_data)
+{
+  EmpathyIndividualView *self = EMPATHY_INDIVIDUAL_VIEW (user_data);
+  EmpathyIndividualViewPriv *priv = GET_PRIV (self);
+  FolksIndividual *individual = NULL;
+  gboolean is_group, is_separator, valid;
+  GtkTreeIter child_iter;
+  gboolean visible;
+
+  if (priv->search_widget == NULL ||
+      !gtk_widget_get_visible (priv->search_widget))
+    return TRUE;
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
+      -1);
+
+  if (individual != NULL)
+    {
+      visible = individual_view_is_visible_individual (self, individual);
+      g_object_unref (individual);
+      return visible;
+    }
+
+  if (is_separator)
+    return TRUE;
+
+  /* Not a contact, not a separator, must be a group */
+  g_return_val_if_fail (is_group, FALSE);
+
+  /* only show groups which are not empty */
+  for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
+       valid; valid = gtk_tree_model_iter_next (model, &child_iter))
+    {
+      gtk_tree_model_get (model, &child_iter,
+        EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
+        -1);
+
+      if (individual == NULL)
+        continue;
+
+      visible = individual_view_is_visible_individual (self, individual);
+      g_object_unref (individual);
+
+      /* show group if it has at least one visible contact in it */
+      if (visible)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+individual_view_query_tooltip_cb (EmpathyIndividualView *view,
+    gint x,
+    gint y,
+    gboolean keyboard_mode,
+    GtkTooltip *tooltip,
+    gpointer user_data)
+{
+  FolksIndividual *individual;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+  GtkTreePath *path;
+  static gint running = 0;
+  gboolean ret = FALSE;
+
+  /* Avoid an infinite loop. See GNOME bug #574377 */
+  if (running > 0)
+    {
+      return FALSE;
+    }
+  running++;
+
+  /* Don't show the tooltip if there's already a popup menu */
+  if (gtk_menu_get_for_attach_widget (GTK_WIDGET (view)) != NULL)
+    {
+      goto OUT;
+    }
+
+  if (!gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (view), &x, &y,
+          keyboard_mode, &model, &path, &iter))
+    {
+      goto OUT;
+    }
+
+  gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (view), tooltip, path);
+  gtk_tree_path_free (path);
+
+  gtk_tree_model_get (model, &iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
+  if (individual == NULL)
+    {
+      goto OUT;
+    }
+
+OUT:
+  running--;
+
+  return ret;
+}
+
+static void
+individual_view_handle_drag (EmpathyIndividualView *self,
+    FolksIndividual *individual,
+    const gchar *old_group,
+    const gchar *new_group,
+    GdkDragAction action)
+{
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
+  g_return_if_fail (FOLKS_IS_INDIVIDUAL (individual));
+
+  DEBUG ("individual %s dragged from '%s' to '%s'",
+      folks_individual_get_id (individual), old_group, new_group);
+
+  if (!tp_strdiff (new_group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
+    {
+      /* Mark contact as favourite */
+
+      /* TODO: implement this */
+      DEBUG ("adding individual to favourites not fully implemented");
+
+      return;
+    }
+
+  if (!tp_strdiff (old_group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
+    {
+      /* Remove contact as favourite */
+
+      /* TODO: implement this */
+      DEBUG ("removing individual from favourites not fully " "implemented");
+
+      /* Don't try to remove it */
+      old_group = NULL;
+    }
+
+  if (new_group != NULL)
+    folks_groups_change_group (FOLKS_GROUPS (individual), new_group, TRUE);
+
+  if (old_group != NULL && action == GDK_ACTION_MOVE)
+    folks_groups_change_group (FOLKS_GROUPS (individual), old_group, FALSE);
+}
+
+static gboolean
+group_can_be_modified (const gchar *name,
+    gboolean is_fake_group,
+    gboolean adding)
+{
+  /* Real groups can always be modified */
+  if (!is_fake_group)
+    return TRUE;
+
+  /* The favorite fake group can be modified so users can
+   * add/remove favorites using DnD */
+  if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
+    return TRUE;
+
+  /* We can remove contacts from the 'ungrouped' fake group */
+  if (!adding && !tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_UNGROUPED))
+    return TRUE;
+
+  return FALSE;
+}
+
+static gboolean
+individual_view_contact_drag_received (GtkWidget *self,
+    GdkDragContext *context,
+    GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkSelectionData *selection)
+{
+  EmpathyIndividualViewPriv *priv;
+  EmpathyIndividualManager *manager;
+  FolksIndividual *individual;
+  GtkTreePath *source_path;
+  const gchar *sel_data;
+  gchar *new_group = NULL;
+  gchar *old_group = NULL;
+  gboolean new_group_is_fake, old_group_is_fake = TRUE;
+
+  priv = GET_PRIV (self);
+
+  sel_data = (const gchar *) gtk_selection_data_get_data (selection);
+  new_group = empathy_individual_store_get_parent_group (model, path,
+      NULL, &new_group_is_fake);
+
+  if (!group_can_be_modified (new_group, new_group_is_fake, TRUE))
+    return FALSE;
+
+  /* Get source group information. */
+  if (priv->drag_row)
+    {
+      source_path = gtk_tree_row_reference_get_path (priv->drag_row);
+      if (source_path)
+        {
+          old_group =
+              empathy_individual_store_get_parent_group (model, source_path,
+              NULL, &old_group_is_fake);
+          gtk_tree_path_free (source_path);
+        }
+    }
+
+  if (!group_can_be_modified (old_group, old_group_is_fake, FALSE))
+    return FALSE;
+
+  if (!tp_strdiff (old_group, new_group))
+    {
+      g_free (new_group);
+      g_free (old_group);
+      return FALSE;
+    }
+
+  /* XXX: for contacts, we used to ensure the account, create the contact
+   * factory, and then wait on the contacts. But they should already be
+   * created by this point */
+
+  manager = empathy_individual_manager_dup_singleton ();
+  individual = empathy_individual_manager_lookup_member (manager, sel_data);
+
+  if (individual == NULL)
+    {
+      DEBUG ("failed to find drag event individual with ID '%s'", sel_data);
+
+      g_object_unref (manager);
+
+      return FALSE;
+    }
+
+  /* FIXME: We should probably wait for the cb before calling
+   * gtk_drag_finish */
+
+  individual_view_handle_drag (EMPATHY_INDIVIDUAL_VIEW (self), individual,
+      old_group, new_group, gdk_drag_context_get_selected_action (context));
+
+  g_object_unref (G_OBJECT (manager));
+  g_free (old_group);
+  g_free (new_group);
+
+  return TRUE;
+}
+
+static gboolean
+individual_view_file_drag_received (GtkWidget *view,
+    GdkDragContext *context,
+    GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkSelectionData *selection)
+{
+  GtkTreeIter iter;
+  const gchar *sel_data;
+  FolksIndividual *individual;
+
+  sel_data = (const gchar *) gtk_selection_data_get_data (selection);
+
+  gtk_tree_model_get_iter (model, &iter, path);
+  gtk_tree_model_get (model, &iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
+  if (!individual)
+    {
+      return FALSE;
+    }
+
+  /* TODO: implement this */
+  DEBUG ("file transfer not implemented");
+
+  g_object_unref (individual);
+
+  return TRUE;
+}
+
+static void
+individual_view_drag_data_received (GtkWidget *view,
+    GdkDragContext *context,
+    gint x,
+    gint y,
+    GtkSelectionData *selection,
+    guint info,
+    guint time_)
+{
+  GtkTreeModel *model;
+  gboolean is_row;
+  GtkTreeViewDropPosition position;
+  GtkTreePath *path;
+  gboolean success = TRUE;
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
+
+  /* Get destination group information. */
+  is_row = gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (view),
+      x, y, &path, &position);
+  if (!is_row)
+    {
+      success = FALSE;
+    }
+  else if (info == DND_DRAG_TYPE_INDIVIDUAL_ID
+      || info == DND_DRAG_TYPE_STRING)
+    {
+      success = individual_view_contact_drag_received (view,
+          context, model, path, selection);
+    }
+  else if (info == DND_DRAG_TYPE_URI_LIST)
+    {
+      success = individual_view_file_drag_received (view,
+          context, model, path, selection);
+    }
+
+  gtk_tree_path_free (path);
+  gtk_drag_finish (context, success, FALSE, GDK_CURRENT_TIME);
+}
+
+static gboolean
+individual_view_drag_motion_cb (DragMotionData *data)
+{
+  gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view), data->path, FALSE);
+
+  data->timeout_id = 0;
+
+  return FALSE;
+}
+
+static gboolean
+individual_view_drag_motion (GtkWidget *widget,
+    GdkDragContext *context,
+    gint x,
+    gint y,
+    guint time_)
+{
+  EmpathyIndividualViewPriv *priv;
+  GtkTreeModel *model;
+  GdkAtom target;
+  GtkTreeIter iter;
+  static DragMotionData *dm = NULL;
+  GtkTreePath *path;
+  gboolean is_row;
+  gboolean is_different = FALSE;
+  gboolean cleanup = TRUE;
+  gboolean retval = TRUE;
+
+  priv = GET_PRIV (EMPATHY_INDIVIDUAL_VIEW (widget));
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
+
+  is_row = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget),
+      x, y, &path, NULL, NULL, NULL);
+
+  cleanup &= (!dm);
+
+  if (is_row)
+    {
+      cleanup &= (dm && gtk_tree_path_compare (dm->path, path) != 0);
+      is_different = (!dm || (dm
+              && gtk_tree_path_compare (dm->path, path) != 0));
+    }
+  else
+    {
+      cleanup &= FALSE;
+    }
+
+  if (path == NULL)
+    {
+      /* Coordinates don't point to an actual row, so make sure the pointer
+         and highlighting don't indicate that a drag is possible.
+       */
+      gdk_drag_status (context, GDK_ACTION_DEFAULT, time_);
+      gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
+      return FALSE;
+    }
+  target = gtk_drag_dest_find_target (widget, context, priv->file_targets);
+  gtk_tree_model_get_iter (model, &iter, path);
+
+  if (target == GDK_NONE)
+    {
+      /* If target == GDK_NONE, then we don't have a target that can be
+         dropped on a contact.  This means a contact drag.  If we're
+         pointing to a group, highlight it.  Otherwise, if the contact
+         we're pointing to is in a group, highlight that.  Otherwise,
+         set the drag position to before the first row for a drag into
+         the "non-group" at the top.
+       */
+      GtkTreeIter group_iter;
+      gboolean is_group;
+      GtkTreePath *group_path;
+      gtk_tree_model_get (model, &iter,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group, -1);
+      if (is_group)
+        {
+          group_iter = iter;
+        }
+      else
+        {
+          if (gtk_tree_model_iter_parent (model, &group_iter, &iter))
+            gtk_tree_model_get (model, &group_iter,
+                EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group, -1);
+        }
+      if (is_group)
+        {
+          gdk_drag_status (context, GDK_ACTION_MOVE, time_);
+          group_path = gtk_tree_model_get_path (model, &group_iter);
+          gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
+              group_path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
+          gtk_tree_path_free (group_path);
+        }
+      else
+        {
+          group_path = gtk_tree_path_new_first ();
+          gdk_drag_status (context, GDK_ACTION_MOVE, time_);
+          gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
+              group_path, GTK_TREE_VIEW_DROP_BEFORE);
+        }
+    }
+  else
+    {
+      /* This is a file drag, and it can only be dropped on contacts,
+         not groups.
+       */
+      FolksIndividual *individual;
+      gtk_tree_model_get (model, &iter,
+          EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
+      if (individual != NULL &&
+          folks_individual_is_online (individual) &&
+          (folks_individual_get_capabilities (individual) &
+              FOLKS_CAPABILITIES_FLAGS_FILE_TRANSFER))
+        {
+          gdk_drag_status (context, GDK_ACTION_COPY, time_);
+          gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
+              path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
+          g_object_unref (individual);
+        }
+      else
+        {
+          gdk_drag_status (context, 0, time_);
+          gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
+          retval = FALSE;
+        }
+    }
+
+  if (!is_different && !cleanup)
+    {
+      return retval;
+    }
+
+  if (dm)
+    {
+      gtk_tree_path_free (dm->path);
+      if (dm->timeout_id)
+        {
+          g_source_remove (dm->timeout_id);
+        }
+
+      g_free (dm);
+
+      dm = NULL;
+    }
+
+  if (!gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path))
+    {
+      dm = g_new0 (DragMotionData, 1);
+
+      dm->view = EMPATHY_INDIVIDUAL_VIEW (widget);
+      dm->path = gtk_tree_path_copy (path);
+
+      dm->timeout_id = g_timeout_add_seconds (1,
+          (GSourceFunc) individual_view_drag_motion_cb, dm);
+    }
+
+  return retval;
+}
+
+static void
+individual_view_drag_begin (GtkWidget *widget,
+    GdkDragContext *context)
+{
+  EmpathyIndividualViewPriv *priv;
+  GtkTreeSelection *selection;
+  GtkTreeModel *model;
+  GtkTreePath *path;
+  GtkTreeIter iter;
+
+  priv = GET_PRIV (widget);
+
+  GTK_WIDGET_CLASS (empathy_individual_view_parent_class)->drag_begin (widget,
+      context);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
+  if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+    {
+      return;
+    }
+
+  path = gtk_tree_model_get_path (model, &iter);
+  priv->drag_row = gtk_tree_row_reference_new (model, path);
+  gtk_tree_path_free (path);
+}
+
+static void
+individual_view_drag_data_get (GtkWidget *widget,
+    GdkDragContext *context,
+    GtkSelectionData *selection,
+    guint info,
+    guint time_)
+{
+  EmpathyIndividualViewPriv *priv;
+  GtkTreePath *src_path;
+  GtkTreeIter iter;
+  GtkTreeModel *model;
+  FolksIndividual *individual;
+  const gchar *individual_id;
+
+  priv = GET_PRIV (widget);
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
+  if (!priv->drag_row)
+    {
+      return;
+    }
+
+  src_path = gtk_tree_row_reference_get_path (priv->drag_row);
+  if (!src_path)
+    {
+      return;
+    }
+
+  if (!gtk_tree_model_get_iter (model, &iter, src_path))
+    {
+      gtk_tree_path_free (src_path);
+      return;
+    }
+
+  gtk_tree_path_free (src_path);
+
+  individual =
+      empathy_individual_view_dup_selected (EMPATHY_INDIVIDUAL_VIEW (widget));
+  if (!individual)
+    {
+      return;
+    }
+
+  individual_id = folks_individual_get_id (individual);
+
+  switch (info)
+    {
+    case DND_DRAG_TYPE_INDIVIDUAL_ID:
+      gtk_selection_data_set (selection, drag_atoms_source[info], 8,
+          (guchar *) individual_id, strlen (individual_id) + 1);
+      break;
+    }
+}
+
+static void
+individual_view_drag_end (GtkWidget *widget,
+    GdkDragContext *context)
+{
+  EmpathyIndividualViewPriv *priv;
+
+  priv = GET_PRIV (widget);
+
+  GTK_WIDGET_CLASS (empathy_individual_view_parent_class)->drag_end (widget,
+      context);
+
+  if (priv->drag_row)
+    {
+      gtk_tree_row_reference_free (priv->drag_row);
+      priv->drag_row = NULL;
+    }
+}
+
+static gboolean
+individual_view_drag_drop (GtkWidget *widget,
+    GdkDragContext *drag_context,
+    gint x,
+    gint y,
+    guint time_)
+{
+  return FALSE;
+}
+
+typedef struct
+{
+  EmpathyIndividualView *view;
+  guint button;
+  guint32 time;
+} MenuPopupData;
+
+static gboolean
+individual_view_popup_menu_idle_cb (gpointer user_data)
+{
+  MenuPopupData *data = user_data;
+  GtkWidget *menu;
+
+  menu = empathy_individual_view_get_individual_menu (data->view);
+  if (!menu)
+    {
+      menu = empathy_individual_view_get_group_menu (data->view);
+    }
+
+  if (menu)
+    {
+      g_signal_connect (menu, "deactivate",
+          G_CALLBACK (gtk_menu_detach), NULL);
+      gtk_menu_attach_to_widget (GTK_MENU (menu),
+          GTK_WIDGET (data->view), NULL);
+      gtk_widget_show (menu);
+      gtk_menu_popup (GTK_MENU (menu),
+          NULL, NULL, NULL, NULL, data->button, data->time);
+      g_object_ref_sink (menu);
+      g_object_unref (menu);
+    }
+
+  g_slice_free (MenuPopupData, data);
+
+  return FALSE;
+}
+
+static gboolean
+individual_view_button_press_event_cb (EmpathyIndividualView *view,
+    GdkEventButton *event,
+    gpointer user_data)
+{
+  if (event->button == 3)
+    {
+      MenuPopupData *data;
+
+      data = g_slice_new (MenuPopupData);
+      data->view = view;
+      data->button = event->button;
+      data->time = event->time;
+      g_idle_add (individual_view_popup_menu_idle_cb, data);
+    }
+
+  return FALSE;
+}
+
+static gboolean
+individual_view_key_press_event_cb (EmpathyIndividualView *view,
+    GdkEventKey *event,
+    gpointer user_data)
+{
+  if (event->keyval == GDK_Menu)
+    {
+      MenuPopupData *data;
+
+      data = g_slice_new (MenuPopupData);
+      data->view = view;
+      data->button = 0;
+      data->time = event->time;
+      g_idle_add (individual_view_popup_menu_idle_cb, data);
+    }
+
+  return FALSE;
+}
+
+static void
+individual_view_row_activated (GtkTreeView *view,
+    GtkTreePath *path,
+    GtkTreeViewColumn *column)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  FolksIndividual *individual;
+  EmpathyContact *contact = NULL;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  if (!(priv->individual_features & EMPATHY_CONTACT_FEATURE_CHAT))
+    {
+      return;
+    }
+
+  model = GTK_TREE_MODEL (priv->store);
+  gtk_tree_model_get_iter (model, &iter, path);
+  gtk_tree_model_get (model, &iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
+
+  if (!individual)
+    return;
+
+  contact = empathy_contact_from_folks_individual (individual);
+
+  if (contact)
+    {
+      DEBUG ("Starting a chat");
+
+      empathy_dispatcher_chat_with_contact (contact,
+          gtk_get_current_event_time (), NULL, NULL);
+    }
+
+  g_object_unref (individual);
+}
+
+static void
+individual_view_call_activated_cb (EmpathyCellRendererActivatable *cell,
+    const gchar *path_string,
+    EmpathyIndividualView *view)
+{
+  GtkWidget *menu;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+  FolksIndividual *individual;
+  GdkEventButton *event;
+  GtkMenuShell *shell;
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
+  if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string))
+    return;
+
+  gtk_tree_model_get (model, &iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
+  if (individual == NULL)
+    return;
+
+  event = (GdkEventButton *) gtk_get_current_event ();
+
+  menu = gtk_menu_new ();
+  shell = GTK_MENU_SHELL (menu);
+
+  /* audio */
+  /* TODO: implement */
+  DEBUG ("audio call menu item unimplemented");
+
+  /* video */
+  /* TODO: implement */
+  DEBUG ("video call menu item unimplemented");
+
+  g_signal_connect (menu, "deactivate", G_CALLBACK (gtk_menu_detach), NULL);
+  gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (view), NULL);
+  gtk_widget_show (menu);
+  gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
+      event->button, event->time);
+  g_object_ref_sink (menu);
+  g_object_unref (menu);
+
+  g_object_unref (individual);
+}
+
+static void
+individual_view_cell_set_background (EmpathyIndividualView *view,
+    GtkCellRenderer *cell,
+    gboolean is_group,
+    gboolean is_active)
+{
+  GdkColor color;
+  GtkStyle *style;
+
+  style = gtk_widget_get_style (GTK_WIDGET (view));
+
+  if (!is_group && is_active)
+    {
+      color = style->bg[GTK_STATE_SELECTED];
+
+      /* Here we take the current theme colour and add it to
+       * the colour for white and average the two. This
+       * gives a colour which is inline with the theme but
+       * slightly whiter.
+       */
+      color.red = (color.red + (style->white).red) / 2;
+      color.green = (color.green + (style->white).green) / 2;
+      color.blue = (color.blue + (style->white).blue) / 2;
+
+      g_object_set (cell, "cell-background-gdk", &color, NULL);
+    }
+  else
+    {
+      g_object_set (cell, "cell-background-gdk", NULL, NULL);
+    }
+}
+
+static void
+individual_view_pixbuf_cell_data_func (GtkTreeViewColumn *tree_column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    EmpathyIndividualView *view)
+{
+  GdkPixbuf *pixbuf;
+  gboolean is_group;
+  gboolean is_active;
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active,
+      EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS, &pixbuf, -1);
+
+  g_object_set (cell, "visible", !is_group, "pixbuf", pixbuf, NULL);
+
+  if (pixbuf != NULL)
+    {
+      g_object_unref (pixbuf);
+    }
+
+  individual_view_cell_set_background (view, cell, is_group, is_active);
+}
+
+static void
+individual_view_group_icon_cell_data_func (GtkTreeViewColumn *tree_column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    EmpathyIndividualView *view)
+{
+  GdkPixbuf *pixbuf = NULL;
+  gboolean is_group;
+  gchar *name;
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name, -1);
+
+  if (!is_group)
+    goto out;
+
+  if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
+    {
+      pixbuf = empathy_pixbuf_from_icon_name ("emblem-favorite",
+          GTK_ICON_SIZE_MENU);
+    }
+  else if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_PEOPLE_NEARBY))
+    {
+      pixbuf = empathy_pixbuf_from_icon_name ("im-local-xmpp",
+          GTK_ICON_SIZE_MENU);
+    }
+
+out:
+  g_object_set (cell, "visible", pixbuf != NULL, "pixbuf", pixbuf, NULL);
+
+  if (pixbuf != NULL)
+    g_object_unref (pixbuf);
+
+  g_free (name);
+}
+
+static void
+individual_view_audio_call_cell_data_func (GtkTreeViewColumn *tree_column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    EmpathyIndividualView *view)
+{
+  gboolean is_group;
+  gboolean is_active;
+  gboolean can_audio, can_video;
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active,
+      EMPATHY_INDIVIDUAL_STORE_COL_CAN_AUDIO_CALL, &can_audio,
+      EMPATHY_INDIVIDUAL_STORE_COL_CAN_VIDEO_CALL, &can_video, -1);
+
+  g_object_set (cell,
+      "visible", !is_group && (can_audio || can_video),
+      "icon-name", can_video ? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP,
+      NULL);
+
+  individual_view_cell_set_background (view, cell, is_group, is_active);
+}
+
+static void
+individual_view_avatar_cell_data_func (GtkTreeViewColumn *tree_column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    EmpathyIndividualView *view)
+{
+  GdkPixbuf *pixbuf;
+  gboolean show_avatar;
+  gboolean is_group;
+  gboolean is_active;
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR, &pixbuf,
+      EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
+
+  g_object_set (cell,
+      "visible", !is_group && show_avatar, "pixbuf", pixbuf, NULL);
+
+  if (pixbuf)
+    {
+      g_object_unref (pixbuf);
+    }
+
+  individual_view_cell_set_background (view, cell, is_group, is_active);
+}
+
+static void
+individual_view_text_cell_data_func (GtkTreeViewColumn *tree_column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    EmpathyIndividualView *view)
+{
+  gboolean is_group;
+  gboolean is_active;
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
+
+  individual_view_cell_set_background (view, cell, is_group, is_active);
+}
+
+static void
+individual_view_expander_cell_data_func (GtkTreeViewColumn *column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    EmpathyIndividualView *view)
+{
+  gboolean is_group;
+  gboolean is_active;
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
+
+  if (gtk_tree_model_iter_has_child (model, iter))
+    {
+      GtkTreePath *path;
+      gboolean row_expanded;
+
+      path = gtk_tree_model_get_path (model, iter);
+      row_expanded =
+          gtk_tree_view_row_expanded (GTK_TREE_VIEW
+          (gtk_tree_view_column_get_tree_view (column)), path);
+      gtk_tree_path_free (path);
+
+      g_object_set (cell,
+          "visible", TRUE,
+          "expander-style",
+          row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
+          NULL);
+    }
+  else
+    {
+      g_object_set (cell, "visible", FALSE, NULL);
+    }
+
+  individual_view_cell_set_background (view, cell, is_group, is_active);
+}
+
+static void
+individual_view_row_expand_or_collapse_cb (EmpathyIndividualView *view,
+    GtkTreeIter *iter,
+    GtkTreePath *path,
+    gpointer user_data)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  GtkTreeModel *model;
+  gchar *name;
+  gboolean expanded;
+
+  if (!(priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE))
+    return;
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name, -1);
+
+  expanded = GPOINTER_TO_INT (user_data);
+  empathy_contact_group_set_expanded (name, expanded);
+
+  g_free (name);
+}
+
+static gboolean
+individual_view_start_search_cb (EmpathyIndividualView *view,
+    gpointer data)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+
+  if (priv->search_widget == NULL)
+    return FALSE;
+
+  if (gtk_widget_get_visible (GTK_WIDGET (priv->search_widget)))
+    gtk_widget_grab_focus (GTK_WIDGET (priv->search_widget));
+  else
+    gtk_widget_show (GTK_WIDGET (priv->search_widget));
+
+  return TRUE;
+}
+
+static void
+individual_view_search_text_notify_cb (EmpathyLiveSearch *search,
+    GParamSpec *pspec,
+    EmpathyIndividualView *view)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  GtkTreePath *path;
+  GtkTreeViewColumn *focus_column;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+  gboolean set_cursor = FALSE;
+
+  gtk_tree_model_filter_refilter (priv->filter);
+
+  /* Set cursor on the first contact. If it is already set on a group,
+   * set it on its first child contact. Note that first child of a group
+   * is its separator, that's why we actually set to the 2nd
+   */
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
+  gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
+
+  if (path == NULL)
+    {
+      path = gtk_tree_path_new_from_string ("0:1");
+      set_cursor = TRUE;
+    }
+  else if (gtk_tree_path_get_depth (path) < 2)
+    {
+      gboolean is_group;
+
+      gtk_tree_model_get_iter (model, &iter, path);
+      gtk_tree_model_get (model, &iter,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+          -1);
+
+      if (is_group)
+        {
+          gtk_tree_path_down (path);
+          gtk_tree_path_next (path);
+          set_cursor = TRUE;
+        }
+    }
+
+  if (set_cursor)
+    {
+      /* FIXME: Workaround for GTK bug #621651, we have to make sure the path is
+       * valid. */
+      if (gtk_tree_model_get_iter (model, &iter, path))
+        {
+          gtk_tree_view_set_cursor (GTK_TREE_VIEW (view), path, focus_column,
+              FALSE);
+        }
+    }
+
+  gtk_tree_path_free (path);
+}
+
+static void
+individual_view_search_activate_cb (GtkWidget *search,
+  EmpathyIndividualView *view)
+{
+  GtkTreePath *path;
+  GtkTreeViewColumn *focus_column;
+
+  gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
+  if (path != NULL)
+    {
+      gtk_tree_view_row_activated (GTK_TREE_VIEW (view), path, focus_column);
+      gtk_tree_path_free (path);
+
+      gtk_widget_hide (search);
+    }
+}
+
+static void
+individual_view_search_hide_cb (EmpathyLiveSearch *search,
+    EmpathyIndividualView *view)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+  gboolean valid = FALSE;
+
+  /* block expand or collapse handlers, they would write the
+   * expand or collapsed setting to file otherwise */
+  g_signal_handlers_block_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+  g_signal_handlers_block_by_func (view,
+    individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
+
+  /* restore which groups are expanded and which are not */
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
+  for (valid = gtk_tree_model_get_iter_first (model, &iter);
+       valid; valid = gtk_tree_model_iter_next (model, &iter))
+    {
+      gboolean is_group;
+      gchar *name = NULL;
+      GtkTreePath *path;
+
+      gtk_tree_model_get (model, &iter,
+          EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
+          EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+          -1);
+
+      if (!is_group)
+        {
+          g_free (name);
+          continue;
+        }
+
+      path = gtk_tree_model_get_path (model, &iter);
+      if ((priv->view_features &
+            EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
+          empathy_contact_group_get_expanded (name))
+        {
+          gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path, TRUE);
+        }
+      else
+        {
+          gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
+        }
+
+      gtk_tree_path_free (path);
+      g_free (name);
+    }
+
+  /* unblock expand or collapse handlers */
+  g_signal_handlers_unblock_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+  g_signal_handlers_unblock_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
+}
+
+static void
+individual_view_search_show_cb (EmpathyLiveSearch *search,
+    EmpathyIndividualView *view)
+{
+  /* block expand or collapse handlers during expand all, they would
+   * write the expand or collapsed setting to file otherwise */
+  g_signal_handlers_block_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+
+  gtk_tree_view_expand_all (GTK_TREE_VIEW (view));
+
+  g_signal_handlers_unblock_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+}
+
+typedef struct {
+  EmpathyIndividualView *view;
+  GtkTreeRowReference *row_ref;
+  gboolean expand;
+} ExpandData;
+
+static gboolean
+individual_view_expand_idle_cb (gpointer user_data)
+{
+  ExpandData *data = user_data;
+  GtkTreePath *path;
+
+  path = gtk_tree_row_reference_get_path (data->row_ref);
+  if (path == NULL)
+    goto done;
+
+  g_signal_handlers_block_by_func (data->view,
+    individual_view_row_expand_or_collapse_cb,
+    GINT_TO_POINTER (data->expand));
+
+  if (data->expand)
+    gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view), path, TRUE);
+  else
+    gtk_tree_view_collapse_row (GTK_TREE_VIEW (data->view), path);
+
+  gtk_tree_path_free (path);
+
+  g_signal_handlers_unblock_by_func (data->view,
+      individual_view_row_expand_or_collapse_cb,
+      GINT_TO_POINTER (data->expand));
+
+done:
+  g_object_unref (data->view);
+  gtk_tree_row_reference_free (data->row_ref);
+  g_slice_free (ExpandData, data);
+
+  return FALSE;
+}
+
+static void
+individual_view_row_has_child_toggled_cb (GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkTreeIter *iter,
+    EmpathyIndividualView *view)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  gboolean is_group = FALSE;
+  gchar *name = NULL;
+  ExpandData *data;
+
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
+      -1);
+
+  if (!is_group || EMP_STR_EMPTY (name))
+    {
+      g_free (name);
+      return;
+    }
+
+  data = g_slice_new0 (ExpandData);
+  data->view = g_object_ref (view);
+  data->row_ref = gtk_tree_row_reference_new (model, path);
+  data->expand =
+      (priv->view_features &
+          EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
+      (priv->search_widget != NULL &&
+          gtk_widget_get_visible (priv->search_widget)) ||
+      empathy_contact_group_get_expanded (name);
+
+  /* FIXME: It doesn't work to call gtk_tree_view_expand_row () from within
+   * gtk_tree_model_filter_refilter () */
+  g_idle_add (individual_view_expand_idle_cb, data);
+
+  g_free (name);
+}
+
+static void
+individual_view_verify_group_visibility (EmpathyIndividualView *view,
+    GtkTreePath *path)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  GtkTreeModel *model;
+  GtkTreePath *parent_path;
+  GtkTreeIter parent_iter;
+
+  if (gtk_tree_path_get_depth (path) < 2)
+    return;
+
+  /* A group row is visible if and only if at least one if its child is visible.
+   * So when a row is inserted/deleted/changed in the base model, that could
+   * modify the visibility of its parent in the filter model.
+  */
+
+  model = GTK_TREE_MODEL (priv->store);
+  parent_path = gtk_tree_path_copy (path);
+  gtk_tree_path_up (parent_path);
+  if (gtk_tree_model_get_iter (model, &parent_iter, parent_path))
+    {
+      /* This tells the filter to verify the visibility of that row, and
+       * show/hide it if necessary */
+      gtk_tree_model_row_changed (GTK_TREE_MODEL (priv->store),
+              parent_path, &parent_iter);
+    }
+  gtk_tree_path_free (parent_path);
+}
+
+static void
+individual_view_store_row_changed_cb (GtkTreeModel *model,
+  GtkTreePath *path,
+  GtkTreeIter *iter,
+  EmpathyIndividualView *view)
+{
+  individual_view_verify_group_visibility (view, path);
+}
+
+static void
+individual_view_store_row_deleted_cb (GtkTreeModel *model,
+  GtkTreePath *path,
+  EmpathyIndividualView *view)
+{
+  individual_view_verify_group_visibility (view, path);
+}
+
+static void
+individual_view_constructed (GObject *object)
+{
+  EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+
+  GtkCellRenderer *cell;
+  GtkTreeViewColumn *col;
+  guint i;
+
+  priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
+      GTK_TREE_MODEL (priv->store), NULL));
+  gtk_tree_model_filter_set_visible_func (priv->filter,
+      individual_view_filter_visible_func, view, NULL);
+
+  g_signal_connect (priv->store, "row-has-child-toggled",
+      G_CALLBACK (individual_view_row_has_child_toggled_cb), view);
+  gtk_tree_view_set_model (GTK_TREE_VIEW (view),
+      GTK_TREE_MODEL (priv->filter));
+
+  tp_g_signal_connect_object (priv->store, "row-changed",
+      G_CALLBACK (individual_view_store_row_changed_cb), view, 0);
+  tp_g_signal_connect_object (priv->store, "row-inserted",
+      G_CALLBACK (individual_view_store_row_changed_cb), view, 0);
+  tp_g_signal_connect_object (priv->store, "row-deleted",
+      G_CALLBACK (individual_view_store_row_deleted_cb), view, 0);
+
+  /* Setup view */
+  /* Setting reorderable is a hack that gets us row previews as drag icons
+     for free.  We override all the drag handlers.  It's tricky to get the
+     position of the drag icon right in drag_begin.  GtkTreeView has special
+     voodoo for it, so we let it do the voodoo that he do.
+   */
+  g_object_set (view,
+      "headers-visible", FALSE,
+      "reorderable", TRUE,
+      "show-expanders", FALSE,
+      NULL);
+
+  col = gtk_tree_view_column_new ();
+
+  /* State */
+  cell = gtk_cell_renderer_pixbuf_new ();
+  gtk_tree_view_column_pack_start (col, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_pixbuf_cell_data_func,
+      view, NULL);
+
+  g_object_set (cell, "xpad", 5, "ypad", 1, "visible", FALSE, NULL);
+
+  /* Group icon */
+  cell = gtk_cell_renderer_pixbuf_new ();
+  gtk_tree_view_column_pack_start (col, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_group_icon_cell_data_func,
+      view, NULL);
+
+  g_object_set (cell,
+      "xpad", 0,
+      "ypad", 0, "visible", FALSE, "width", 16, "height", 16, NULL);
+
+  /* Name */
+  cell = empathy_cell_renderer_text_new ();
+  gtk_tree_view_column_pack_start (col, cell, TRUE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_text_cell_data_func, view, NULL);
+
+  gtk_tree_view_column_add_attribute (col, cell,
+      "name", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
+  gtk_tree_view_column_add_attribute (col, cell,
+      "text", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
+  gtk_tree_view_column_add_attribute (col, cell,
+      "presence-type", EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE);
+  gtk_tree_view_column_add_attribute (col, cell,
+      "status", EMPATHY_INDIVIDUAL_STORE_COL_STATUS);
+  gtk_tree_view_column_add_attribute (col, cell,
+      "is_group", EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP);
+  gtk_tree_view_column_add_attribute (col, cell,
+      "compact", EMPATHY_INDIVIDUAL_STORE_COL_COMPACT);
+
+  /* Audio Call Icon */
+  cell = empathy_cell_renderer_activatable_new ();
+  gtk_tree_view_column_pack_start (col, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_audio_call_cell_data_func,
+      view, NULL);
+
+  g_object_set (cell, "visible", FALSE, NULL);
+
+  g_signal_connect (cell, "path-activated",
+      G_CALLBACK (individual_view_call_activated_cb), view);
+
+  /* Avatar */
+  cell = gtk_cell_renderer_pixbuf_new ();
+  gtk_tree_view_column_pack_start (col, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_avatar_cell_data_func,
+      view, NULL);
+
+  g_object_set (cell,
+      "xpad", 0,
+      "ypad", 0, "visible", FALSE, "width", 32, "height", 32, NULL);
+
+  /* Expander */
+  cell = empathy_cell_renderer_expander_new ();
+  gtk_tree_view_column_pack_end (col, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_expander_cell_data_func,
+      view, NULL);
+
+  /* Actually add the column now we have added all cell renderers */
+  gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
+
+  /* Drag & Drop. */
+  for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
+    {
+      drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
+    }
+
+  for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i)
+    {
+      drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target,
+          FALSE);
+    }
+}
+
+static void
+individual_view_set_view_features (EmpathyIndividualView *view,
+    EmpathyIndividualFeatureFlags features)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  gboolean has_tooltip;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view));
+
+  priv->view_features = features;
+
+  /* Update DnD source/dest */
+  if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DRAG)
+    {
+      gtk_drag_source_set (GTK_WIDGET (view),
+          GDK_BUTTON1_MASK,
+          drag_types_source,
+          G_N_ELEMENTS (drag_types_source),
+          GDK_ACTION_MOVE | GDK_ACTION_COPY);
+    }
+  else
+    {
+      gtk_drag_source_unset (GTK_WIDGET (view));
+
+    }
+
+  if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DROP)
+    {
+      gtk_drag_dest_set (GTK_WIDGET (view),
+          GTK_DEST_DEFAULT_ALL,
+          drag_types_dest,
+          G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
+    }
+  else
+    {
+      /* FIXME: URI could still be droped depending on FT feature */
+      gtk_drag_dest_unset (GTK_WIDGET (view));
+    }
+
+  /* Update has-tooltip */
+  has_tooltip =
+      (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_TOOLTIP) != 0;
+  gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
+}
+
+static void
+individual_view_dispose (GObject *object)
+{
+  EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+
+  if (priv->store != NULL)
+    {
+      g_object_unref (priv->store);
+      priv->store = NULL;
+    }
+  if (priv->filter != NULL)
+    {
+      g_object_unref (priv->filter);
+      priv->filter = NULL;
+    }
+  if (priv->tooltip_widget != NULL)
+    {
+      gtk_widget_destroy (priv->tooltip_widget);
+      priv->tooltip_widget = NULL;
+    }
+  if (priv->file_targets != NULL)
+    {
+      gtk_target_list_unref (priv->file_targets);
+      priv->file_targets = NULL;
+    }
+
+  empathy_individual_view_set_live_search (view, NULL);
+
+  G_OBJECT_CLASS (empathy_individual_view_parent_class)->dispose (object);
+}
+
+static void
+individual_view_get_property (GObject *object,
+    guint param_id,
+    GValue *value,
+    GParamSpec *pspec)
+{
+  EmpathyIndividualViewPriv *priv;
+
+  priv = GET_PRIV (object);
+
+  switch (param_id)
+    {
+    case PROP_STORE:
+      g_value_set_object (value, priv->store);
+      break;
+    case PROP_VIEW_FEATURES:
+      g_value_set_flags (value, priv->view_features);
+      break;
+    case PROP_INDIVIDUAL_FEATURES:
+      g_value_set_flags (value, priv->individual_features);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+      break;
+    };
+}
+
+static void
+individual_view_set_property (GObject *object,
+    guint param_id,
+    const GValue *value,
+    GParamSpec *pspec)
+{
+  EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
+  EmpathyIndividualViewPriv *priv = GET_PRIV (object);
+
+  switch (param_id)
+    {
+    case PROP_STORE:
+      priv->store = g_value_dup_object (value);
+      break;
+    case PROP_VIEW_FEATURES:
+      individual_view_set_view_features (view, g_value_get_flags (value));
+      break;
+    case PROP_INDIVIDUAL_FEATURES:
+      priv->individual_features = g_value_get_flags (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+      break;
+    };
+}
+
+static void
+empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
+
+  object_class->constructed = individual_view_constructed;
+  object_class->dispose = individual_view_dispose;
+  object_class->get_property = individual_view_get_property;
+  object_class->set_property = individual_view_set_property;
+
+  widget_class->drag_data_received = individual_view_drag_data_received;
+  widget_class->drag_drop = individual_view_drag_drop;
+  widget_class->drag_begin = individual_view_drag_begin;
+  widget_class->drag_data_get = individual_view_drag_data_get;
+  widget_class->drag_end = individual_view_drag_end;
+  widget_class->drag_motion = individual_view_drag_motion;
+
+  /* We use the class method to let user of this widget to connect to
+   * the signal and stop emission of the signal so the default handler
+   * won't be called. */
+  tree_view_class->row_activated = individual_view_row_activated;
+
+  signals[DRAG_CONTACT_RECEIVED] =
+      g_signal_new ("drag-contact-received",
+      G_OBJECT_CLASS_TYPE (klass),
+      G_SIGNAL_RUN_LAST,
+      0,
+      NULL, NULL,
+      _empathy_gtk_marshal_VOID__OBJECT_STRING_STRING,
+      G_TYPE_NONE, 3, EMPATHY_TYPE_CONTACT, G_TYPE_STRING, G_TYPE_STRING);
+
+  g_object_class_install_property (object_class,
+      PROP_STORE,
+      g_param_spec_object ("store",
+          "The store of the view",
+          "The store of the view",
+          EMPATHY_TYPE_INDIVIDUAL_STORE,
+          G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_VIEW_FEATURES,
+      g_param_spec_flags ("view-features",
+          "Features of the view",
+          "Flags for all enabled features",
+          EMPATHY_TYPE_INDIVIDUAL_VIEW_FEATURE_FLAGS,
+          EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE, G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_INDIVIDUAL_FEATURES,
+      g_param_spec_flags ("individual-features",
+          "Features of the contact menu",
+          "Flags for all enabled features for the menu",
+          EMPATHY_TYPE_INDIVIDUAL_FEATURE_FLAGS,
+          EMPATHY_CONTACT_FEATURE_NONE, G_PARAM_READWRITE));
+
+  g_type_class_add_private (object_class, sizeof (EmpathyIndividualViewPriv));
+}
+
+static void
+empathy_individual_view_init (EmpathyIndividualView *view)
+{
+  EmpathyIndividualViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
+      EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewPriv);
+
+  view->priv = priv;
+  /* Get saved group states. */
+  empathy_contact_groups_get_all ();
+
+  gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
+      empathy_individual_store_row_separator_func, NULL, NULL);
+
+  /* Set up drag target lists. */
+  priv->file_targets = gtk_target_list_new (drag_types_dest_file,
+      G_N_ELEMENTS (drag_types_dest_file));
+
+  /* Connect to tree view signals rather than override. */
+  g_signal_connect (view, "button-press-event",
+      G_CALLBACK (individual_view_button_press_event_cb), NULL);
+  g_signal_connect (view, "key-press-event",
+      G_CALLBACK (individual_view_key_press_event_cb), NULL);
+  g_signal_connect (view, "row-expanded",
+      G_CALLBACK (individual_view_row_expand_or_collapse_cb),
+      GINT_TO_POINTER (TRUE));
+  g_signal_connect (view, "row-collapsed",
+      G_CALLBACK (individual_view_row_expand_or_collapse_cb),
+      GINT_TO_POINTER (FALSE));
+  g_signal_connect (view, "query-tooltip",
+      G_CALLBACK (individual_view_query_tooltip_cb), NULL);
+}
+
+EmpathyIndividualView *
+empathy_individual_view_new (EmpathyIndividualStore *store,
+    EmpathyIndividualViewFeatureFlags view_features,
+    EmpathyIndividualFeatureFlags individual_features)
+{
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (store), NULL);
+
+  return g_object_new (EMPATHY_TYPE_INDIVIDUAL_VIEW,
+      "store", store,
+      "individual-features", individual_features,
+      "view-features", view_features, NULL);
+}
+
+FolksIndividual *
+empathy_individual_view_dup_selected (EmpathyIndividualView *view)
+{
+  EmpathyIndividualViewPriv *priv;
+  GtkTreeSelection *selection;
+  GtkTreeIter iter;
+  GtkTreeModel *model;
+  FolksIndividual *individual;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
+
+  priv = GET_PRIV (view);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
+  if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+    {
+      return NULL;
+    }
+
+  gtk_tree_model_get (model, &iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
+
+  return individual;
+}
+
+EmpathyIndividualManagerFlags
+empathy_individual_view_get_flags (EmpathyIndividualView *view)
+{
+  EmpathyIndividualViewPriv *priv;
+  GtkTreeSelection *selection;
+  GtkTreeIter iter;
+  GtkTreeModel *model;
+  EmpathyIndividualFeatureFlags flags;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), 0);
+
+  priv = GET_PRIV (view);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
+  if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+    {
+      return 0;
+    }
+
+  gtk_tree_model_get (model, &iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_FLAGS, &flags, -1);
+
+  return flags;
+}
+
+gchar *
+empathy_individual_view_get_selected_group (EmpathyIndividualView *view,
+    gboolean *is_fake_group)
+{
+  EmpathyIndividualViewPriv *priv;
+  GtkTreeSelection *selection;
+  GtkTreeIter iter;
+  GtkTreeModel *model;
+  gboolean is_group;
+  gchar *name;
+  gboolean fake;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
+
+  priv = GET_PRIV (view);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
+  if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+    {
+      return NULL;
+    }
+
+  gtk_tree_model_get (model, &iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake, -1);
+
+  if (!is_group)
+    {
+      g_free (name);
+      return NULL;
+    }
+
+  if (is_fake_group != NULL)
+    *is_fake_group = fake;
+
+  return name;
+}
+
+static gboolean
+individual_view_remove_dialog_show (GtkWindow *parent,
+    const gchar *message,
+    const gchar *secondary_text)
+{
+  GtkWidget *dialog;
+  gboolean res;
+
+  dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
+      GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message);
+  gtk_dialog_add_buttons (GTK_DIALOG (dialog),
+      GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
+      GTK_STOCK_DELETE, GTK_RESPONSE_YES, NULL);
+  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+      "%s", secondary_text);
+
+  gtk_widget_show (dialog);
+
+  res = gtk_dialog_run (GTK_DIALOG (dialog));
+  gtk_widget_destroy (dialog);
+
+  return (res == GTK_RESPONSE_YES);
+}
+
+static void
+individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
+    EmpathyIndividualView *view)
+{
+  gchar *group;
+
+  group = empathy_individual_view_get_selected_group (view, NULL);
+  if (group)
+    {
+      gchar *text;
+      GtkWindow *parent;
+
+      text =
+          g_strdup_printf (_("Do you really want to remove the group '%s'?"),
+          group);
+      parent = empathy_get_toplevel_window (GTK_WIDGET (view));
+      if (individual_view_remove_dialog_show (parent, _("Removing group"),
+              text))
+        {
+          /* TODO: implement */
+          DEBUG ("removing group unimplemented");
+        }
+
+      g_free (text);
+    }
+
+  g_free (group);
+}
+
+GtkWidget *
+empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  gchar *group;
+  GtkWidget *menu;
+  GtkWidget *item;
+  GtkWidget *image;
+  gboolean is_fake_group;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
+
+  if (!(priv->view_features & (EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
+              EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)))
+    {
+      return NULL;
+    }
+
+  group = empathy_individual_view_get_selected_group (view, &is_fake_group);
+  if (!group || is_fake_group)
+    {
+      /* We can't alter fake groups */
+      return NULL;
+    }
+
+  menu = gtk_menu_new ();
+
+  /* TODO: implement
+     if (priv->view_features &
+     EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME) {
+     item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
+     gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+     gtk_widget_show (item);
+     g_signal_connect (item, "activate",
+     G_CALLBACK (individual_view_group_rename_activate_cb),
+     view);
+     }
+   */
+
+  if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)
+    {
+      item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
+      image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
+          GTK_ICON_SIZE_MENU);
+      gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
+      gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+      gtk_widget_show (item);
+      g_signal_connect (item, "activate",
+          G_CALLBACK (individual_view_group_remove_activate_cb), view);
+    }
+
+  g_free (group);
+
+  return menu;
+}
+
+static void
+individual_view_remove_activate_cb (GtkMenuItem *menuitem,
+    EmpathyIndividualView *view)
+{
+  FolksIndividual *individual;
+
+  individual = empathy_individual_view_dup_selected (view);
+
+  if (individual)
+    {
+      gchar *text;
+      GtkWindow *parent;
+
+      parent = empathy_get_toplevel_window (GTK_WIDGET (view));
+      text =
+          g_strdup_printf (_
+          ("Do you really want to remove the contact '%s'?"),
+          folks_individual_get_alias (individual));
+      if (individual_view_remove_dialog_show (parent, _("Removing contact"),
+              text))
+        {
+          EmpathyIndividualManager *manager;
+
+          manager = empathy_individual_manager_dup_singleton ();
+          empathy_individual_manager_remove (manager, individual, "");
+          g_object_unref (G_OBJECT (manager));
+        }
+
+      g_free (text);
+      g_object_unref (individual);
+    }
+}
+
+GtkWidget *
+empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  FolksIndividual *individual;
+  GtkWidget *menu = NULL;
+  GtkWidget *item;
+  GtkWidget *image;
+  EmpathyIndividualManagerFlags flags;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
+
+  individual = empathy_individual_view_dup_selected (view);
+  if (!individual)
+    {
+      return NULL;
+    }
+  flags = empathy_individual_view_get_flags (view);
+
+  /* TODO: implement (create the menu here */
+  DEBUG ("individual menu not implemented");
+
+  /* Remove contact */
+  if (priv->view_features &
+      EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_REMOVE &&
+      flags & EMPATHY_INDIVIDUAL_MANAGER_CAN_REMOVE)
+    {
+
+      /* create the menu if required, or just add a separator */
+      if (!menu)
+        {
+          menu = gtk_menu_new ();
+        }
+      else
+        {
+          item = gtk_separator_menu_item_new ();
+          gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+          gtk_widget_show (item);
+        }
+
+      /* Remove */
+      item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
+      image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
+          GTK_ICON_SIZE_MENU);
+      gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
+      gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+      gtk_widget_show (item);
+      g_signal_connect (item, "activate",
+          G_CALLBACK (individual_view_remove_activate_cb), view);
+    }
+
+  g_object_unref (individual);
+
+  return menu;
+}
+
+void
+empathy_individual_view_set_live_search (EmpathyIndividualView *view,
+    EmpathyLiveSearch *search)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+
+  /* remove old handlers if old search was not null */
+  if (priv->search_widget != NULL)
+    {
+      g_signal_handlers_disconnect_by_func (view,
+          individual_view_start_search_cb, NULL);
+
+      g_signal_handlers_disconnect_by_func (priv->search_widget,
+          individual_view_search_text_notify_cb, view);
+      g_signal_handlers_disconnect_by_func (priv->search_widget,
+          individual_view_search_activate_cb, view);
+      g_signal_handlers_disconnect_by_func (priv->search_widget,
+          individual_view_search_hide_cb, view);
+      g_signal_handlers_disconnect_by_func (priv->search_widget,
+          individual_view_search_show_cb, view);
+      g_object_unref (priv->search_widget);
+      priv->search_widget = NULL;
+    }
+
+  /* connect handlers if new search is not null */
+  if (search != NULL)
+    {
+      priv->search_widget = g_object_ref (search);
+
+      g_signal_connect (view, "start-interactive-search",
+          G_CALLBACK (individual_view_start_search_cb), NULL);
+
+      g_signal_connect (priv->search_widget, "notify::text",
+          G_CALLBACK (individual_view_search_text_notify_cb), view);
+      g_signal_connect (priv->search_widget, "activate",
+          G_CALLBACK (individual_view_search_activate_cb), view);
+      g_signal_connect (priv->search_widget, "hide",
+          G_CALLBACK (individual_view_search_hide_cb), view);
+      g_signal_connect (priv->search_widget, "show",
+          G_CALLBACK (individual_view_search_show_cb), view);
+    }
+}
diff --git a/libempathy-gtk/empathy-individual-view.h b/libempathy-gtk/empathy-individual-view.h
new file mode 100644 (file)
index 0000000..826e490
--- /dev/null
@@ -0,0 +1,99 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2005-2007 Imendio AB
+ * Copyright (C) 2007-2010 Collabora Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Authors: Mikael Hallendal <micke@imendio.com>
+ *          Martyn Russell <martyn@imendio.com>
+ *          Xavier Claessens <xclaesse@gmail.com>
+ *          Travis Reitter <travis.reitter@collabora.co.uk>
+ */
+
+#ifndef __EMPATHY_INDIVIDUAL_VIEW_H__
+#define __EMPATHY_INDIVIDUAL_VIEW_H__
+
+#include <gtk/gtk.h>
+
+#include <folks/folks.h>
+
+#include <libempathy/empathy-enum-types.h>
+
+#include "empathy-live-search.h"
+#include "empathy-individual-menu.h"
+#include "empathy-individual-store.h"
+
+G_BEGIN_DECLS
+#define EMPATHY_TYPE_INDIVIDUAL_VIEW         (empathy_individual_view_get_type ())
+#define EMPATHY_INDIVIDUAL_VIEW(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualView))
+#define EMPATHY_INDIVIDUAL_VIEW_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewClass))
+#define EMPATHY_IS_INDIVIDUAL_VIEW(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), EMPATHY_TYPE_INDIVIDUAL_VIEW))
+#define EMPATHY_IS_INDIVIDUAL_VIEW_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), EMPATHY_TYPE_INDIVIDUAL_VIEW))
+#define EMPATHY_INDIVIDUAL_VIEW_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewClass))
+typedef struct _EmpathyIndividualView EmpathyIndividualView;
+typedef struct _EmpathyIndividualViewClass EmpathyIndividualViewClass;
+
+typedef enum
+{
+  EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE = 0,
+  EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE = 1 << 0,
+  EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME = 1 << 1,
+  EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE = 1 << 2,
+  EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_REMOVE = 1 << 3,
+  EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DROP = 1 << 4,
+  EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DRAG = 1 << 5,
+  EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_TOOLTIP = 1 << 6,
+  EMPATHY_INDIVIDUAL_VIEW_FEATURE_ALL = (1 << 7) - 1,
+} EmpathyIndividualViewFeatureFlags;
+
+struct _EmpathyIndividualView
+{
+  GtkTreeView parent;
+  gpointer priv;
+};
+
+struct _EmpathyIndividualViewClass
+{
+  GtkTreeViewClass parent_class;
+};
+
+GType empathy_individual_view_get_type (void) G_GNUC_CONST;
+
+EmpathyIndividualView *empathy_individual_view_new (
+    EmpathyIndividualStore *store,
+    EmpathyIndividualViewFeatureFlags view_features,
+    EmpathyIndividualFeatureFlags individual_features);
+
+FolksIndividual *empathy_individual_view_dup_selected (
+    EmpathyIndividualView *view);
+
+EmpathyIndividualManagerFlags empathy_individual_view_get_flags (
+    EmpathyIndividualView *view);
+
+gchar *empathy_individual_view_get_selected_group (EmpathyIndividualView *view,
+    gboolean * is_fake_group);
+
+GtkWidget *empathy_individual_view_get_individual_menu (
+    EmpathyIndividualView *view);
+
+GtkWidget *empathy_individual_view_get_group_menu (EmpathyIndividualView *view);
+
+void empathy_individual_view_set_live_search (EmpathyIndividualView *view,
+    EmpathyLiveSearch *search);
+
+G_END_DECLS
+#endif /* __EMPATHY_INDIVIDUAL_VIEW_H__ */
index d5abf026518b7386ef1987f501a98f6f6259a378..93c08d1c59425e054851929a31d75c9abdf0f1fb 100644 (file)
@@ -1,7 +1,7 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
 /*
  * Copyright (C) 2002-2007 Imendio AB
- * Copyright (C) 2007-2008 Collabora Ltd.
+ * Copyright (C) 2007-2010 Collabora Ltd.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -23,6 +23,7 @@
  *          Martyn Russell <martyn@imendio.com>
  *          Xavier Claessens <xclaesse@gmail.com>
  *          Jonny Lamb <jonny.lamb@collabora.co.uk>
+ *          Travis Reitter <travis.reitter@collabora.co.uk>
  *
  *          Part of this file is copied from GtkSourceView (gtksourceiter.c):
  *          Paolo Maggi
@@ -38,6 +39,8 @@
 #include <gtk/gtk.h>
 #include <gio/gio.h>
 
+#include <folks/folks.h>
+
 #include "empathy-ui-utils.h"
 #include "empathy-images.h"
 #include "empathy-smiley-manager.h"
@@ -220,6 +223,18 @@ empathy_icon_name_for_contact (EmpathyContact *contact)
        return empathy_icon_name_for_presence (presence);
 }
 
+const gchar *
+empathy_icon_name_for_individual (FolksIndividual *individual)
+{
+       FolksPresenceType folks_presence;
+       TpConnectionPresenceType presence;
+
+       folks_presence = folks_individual_get_presence_type (individual);
+       presence = empathy_folks_presence_type_to_tp (folks_presence);
+
+       return empathy_icon_name_for_presence (presence);
+}
+
 const gchar *
 empathy_protocol_name_for_contact (EmpathyContact   *contact)
 {
@@ -524,7 +539,8 @@ empathy_pixbuf_contact_status_icon_with_icon_name (EmpathyContact *contact,
        gint       height, width;
        gint       numerator, denominator;
 
-       g_return_val_if_fail (EMPATHY_IS_CONTACT (contact), NULL);
+       g_return_val_if_fail (EMPATHY_IS_CONTACT (contact) ||
+                       (show_protocol == FALSE), NULL);
        g_return_val_if_fail (icon_name != NULL, NULL);
 
        numerator = 3;
index e2f3e4b2d8ffd928de33242b9bc30789ec87ac3c..3032aea84469788f9b98f1b1b092bf6a8b6068bf 100644 (file)
@@ -1,7 +1,7 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
 /*
  * Copyright (C) 2002-2007 Imendio AB
- * Copyright (C) 2007-2008 Collabora Ltd.
+ * Copyright (C) 2007-2010 Collabora Ltd.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -23,6 +23,7 @@
  *          Martyn Russell <martyn@imendio.com>
  *          Xavier Claessens <xclaesse@gmail.com>
  *          Jonny Lamb <jonny.lamb@collabora.co.uk>
+ *          Travis Reitter <travis.reitter@collabora.co.uk>
  *
  *          Part of this file is copied from GtkSourceView (gtksourceiter.c):
  *          Paolo Maggi
@@ -34,6 +35,8 @@
 
 #include <gtk/gtk.h>
 
+#include <folks/folks.h>
+
 #include <libempathy/empathy-contact.h>
 #include <libempathy/empathy-ft-handler.h>
 
@@ -62,6 +65,7 @@ GtkWidget     *empathy_builder_unref_and_keep_widget    (GtkBuilder       *gui,
 /* Pixbufs */
 const gchar * empathy_icon_name_for_presence            (TpConnectionPresenceType  presence);
 const gchar * empathy_icon_name_for_contact             (EmpathyContact   *contact);
+const gchar * empathy_icon_name_for_individual          (FolksIndividual  *individual);
 const gchar * empathy_protocol_name_for_contact         (EmpathyContact   *contact);
 GdkPixbuf *   empathy_pixbuf_from_data                  (gchar            *data,
                                                         gsize             data_size);
index 746803e059bfcc2eb55be7792c5aa7598672788c..ca0df9206371d8767b992d0052e5effdc8583d23 100644 (file)
@@ -44,6 +44,7 @@ libempathy_headers =                          \
        empathy-gsettings.h                     \
        empathy-handler.h                       \
        empathy-idle.h                          \
+       empathy-individual-manager.h            \
        empathy-irc-network-manager.h           \
        empathy-irc-network.h                   \
        empathy-irc-server.h                    \
@@ -80,6 +81,7 @@ libempathy_la_SOURCES =                                       \
        empathy-ft-handler.c                            \
        empathy-handler.c                               \
        empathy-idle.c                                  \
+       empathy-individual-manager.c                    \
        empathy-irc-network-manager.c                   \
        empathy-irc-network.c                           \
        empathy-irc-server.c                            \
diff --git a/libempathy/empathy-individual-manager.c b/libempathy/empathy-individual-manager.c
new file mode 100644 (file)
index 0000000..4b27e74
--- /dev/null
@@ -0,0 +1,290 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2007-2010 Collabora Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Authors: Xavier Claessens <xclaesse@gmail.com>
+ *          Travis Reitter <travis.reitter@collabora.co.uk>
+ */
+
+#include <config.h>
+
+#include <string.h>
+
+#include <telepathy-glib/account-manager.h>
+#include <telepathy-glib/enums.h>
+#include <telepathy-glib/proxy-subclass.h>
+#include <telepathy-glib/util.h>
+
+#include <folks/folks.h>
+
+#include <extensions/extensions.h>
+
+#include "empathy-individual-manager.h"
+#include "empathy-contact-manager.h"
+#include "empathy-contact-list.h"
+#include "empathy-marshal.h"
+#include "empathy-utils.h"
+
+#define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
+#include "empathy-debug.h"
+
+#define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyIndividualManager)
+typedef struct
+{
+  FolksIndividualAggregator *aggregator;
+  EmpathyContactManager *contact_manager;
+  TpProxy *logger;
+  /* account object path (gchar *) => GHashTable containing favorite contacts
+   * (contact ID (gchar *) => TRUE) */
+  GHashTable *favourites;
+  TpProxySignalConnection *favourite_contacts_changed_signal;
+} EmpathyIndividualManagerPriv;
+
+G_DEFINE_TYPE (EmpathyIndividualManager, empathy_individual_manager,
+    G_TYPE_OBJECT);
+
+static EmpathyIndividualManager *manager_singleton = NULL;
+
+static void
+individual_group_changed_cb (FolksIndividual *individual,
+    gchar *group,
+    gboolean is_member,
+    EmpathyIndividualManager *self)
+{
+  g_signal_emit_by_name (self, "groups-changed", individual, group,
+      is_member);
+}
+
+static void
+aggregator_individuals_added_cb (FolksIndividualAggregator *aggregator,
+    GList *individuals,
+    EmpathyIndividualManager *self)
+{
+  GList *l;
+
+  for (l = individuals; l; l = l->next)
+    {
+      g_signal_connect (l->data, "group-changed",
+          G_CALLBACK (individual_group_changed_cb), self);
+    }
+
+  /* TODO: don't hard-code the reason or message */
+  g_signal_emit_by_name (self, "members-changed",
+      "individual(s) added", individuals, NULL,
+      TP_CHANNEL_GROUP_CHANGE_REASON_NONE, TRUE);
+}
+
+static void
+aggregator_individuals_removed_cb (FolksIndividualAggregator *aggregator,
+    GList *individuals,
+    EmpathyIndividualManager *self)
+{
+  GList *l;
+
+  for (l = individuals; l; l = l->next)
+    {
+      g_signal_handlers_disconnect_by_func (l->data,
+          individual_group_changed_cb, self);
+    }
+
+  /* TODO: don't hard-code the reason or message */
+  g_signal_emit_by_name (self, "members-changed",
+      "individual(s) removed", NULL, individuals,
+      TP_CHANNEL_GROUP_CHANGE_REASON_NONE, TRUE);
+}
+
+static void
+individual_manager_finalize (GObject *object)
+{
+  EmpathyIndividualManagerPriv *priv = GET_PRIV (object);
+
+  tp_proxy_signal_connection_disconnect (
+      priv->favourite_contacts_changed_signal);
+
+  if (priv->logger != NULL)
+    g_object_unref (priv->logger);
+
+  if (priv->contact_manager != NULL)
+    g_object_unref (priv->contact_manager);
+
+  if (priv->aggregator != NULL)
+    g_object_unref (priv->aggregator);
+
+  g_hash_table_destroy (priv->favourites);
+}
+
+static GObject *
+individual_manager_constructor (GType type,
+    guint n_props,
+    GObjectConstructParam *props)
+{
+  GObject *retval;
+
+  if (manager_singleton)
+    {
+      retval = g_object_ref (manager_singleton);
+    }
+  else
+    {
+      retval =
+          G_OBJECT_CLASS (empathy_individual_manager_parent_class)->
+          constructor (type, n_props, props);
+
+      manager_singleton = EMPATHY_INDIVIDUAL_MANAGER (retval);
+      g_object_add_weak_pointer (retval, (gpointer) & manager_singleton);
+    }
+
+  return retval;
+}
+
+/**
+ * empathy_individual_manager_initialized:
+ *
+ * Reports whether or not the singleton has already been created.
+ *
+ * There can be instances where you want to access the #EmpathyIndividualManager
+ * only if it has been set up for this process.
+ *
+ * Returns: %TRUE if the #EmpathyIndividualManager singleton has previously
+ * been initialized.
+ */
+gboolean
+empathy_individual_manager_initialized (void)
+{
+  return (manager_singleton != NULL);
+}
+
+static void
+empathy_individual_manager_class_init (EmpathyIndividualManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = individual_manager_finalize;
+  object_class->constructor = individual_manager_constructor;
+
+  g_signal_new ("groups-changed",
+      G_TYPE_FROM_CLASS (klass),
+      G_SIGNAL_RUN_LAST,
+      0,
+      NULL, NULL,
+      _empathy_marshal_VOID__OBJECT_STRING_BOOLEAN,
+      G_TYPE_NONE, 3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_STRING, G_TYPE_BOOLEAN);
+
+  g_signal_new ("members-changed",
+      G_TYPE_FROM_CLASS (klass),
+      G_SIGNAL_RUN_LAST,
+      0,
+      NULL, NULL,
+      _empathy_marshal_VOID__STRING_OBJECT_OBJECT_UINT,
+      G_TYPE_NONE,
+      4, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER, G_TYPE_UINT);
+
+  g_type_class_add_private (object_class,
+      sizeof (EmpathyIndividualManagerPriv));
+}
+
+static void
+empathy_individual_manager_init (EmpathyIndividualManager *self)
+{
+  EmpathyIndividualManagerPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      EMPATHY_TYPE_INDIVIDUAL_MANAGER, EmpathyIndividualManagerPriv);
+  TpDBusDaemon *bus;
+  GError *error = NULL;
+
+  self->priv = priv;
+  priv->contact_manager = empathy_contact_manager_dup_singleton ();
+
+  priv->favourites = g_hash_table_new_full (g_str_hash, g_str_equal,
+      (GDestroyNotify) g_free, (GDestroyNotify) g_hash_table_unref);
+
+  priv->aggregator = folks_individual_aggregator_new ();
+  if (error == NULL)
+    {
+      g_signal_connect (priv->aggregator, "individuals-added",
+          G_CALLBACK (aggregator_individuals_added_cb), self);
+      g_signal_connect (priv->aggregator, "individuals-removed",
+          G_CALLBACK (aggregator_individuals_removed_cb), self);
+    }
+  else
+    {
+      DEBUG ("Failed to get individual aggregator: %s", error->message);
+      g_clear_error (&error);
+    }
+
+  bus = tp_dbus_daemon_dup (&error);
+
+  if (error == NULL)
+    {
+      priv->logger = g_object_new (TP_TYPE_PROXY,
+          "bus-name", "org.freedesktop.Telepathy.Logger",
+          "object-path",
+          "/org/freedesktop/Telepathy/Logger", "dbus-daemon", bus, NULL);
+      g_object_unref (bus);
+
+      tp_proxy_add_interface_by_id (priv->logger, EMP_IFACE_QUARK_LOGGER);
+    }
+  else
+    {
+      DEBUG ("Failed to get telepathy-logger proxy: %s", error->message);
+      g_clear_error (&error);
+    }
+}
+
+EmpathyIndividualManager *
+empathy_individual_manager_dup_singleton (void)
+{
+  return g_object_new (EMPATHY_TYPE_INDIVIDUAL_MANAGER, NULL);
+}
+
+/* TODO: support adding and removing Individuals */
+
+GList *
+empathy_individual_manager_get_members (EmpathyIndividualManager *self)
+{
+  EmpathyIndividualManagerPriv *priv = GET_PRIV (self);
+  GHashTable *individuals;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self), NULL);
+
+  individuals = folks_individual_aggregator_get_individuals (priv->aggregator);
+  return individuals ? g_hash_table_get_values (individuals) : NULL;
+}
+
+FolksIndividual *
+empathy_individual_manager_lookup_member (EmpathyIndividualManager *self,
+    const gchar *id)
+{
+  EmpathyIndividualManagerPriv *priv = GET_PRIV (self);
+  GHashTable *individuals;
+
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (self), NULL);
+
+  individuals = folks_individual_aggregator_get_individuals (priv->aggregator);
+  if (individuals != NULL)
+    return g_hash_table_lookup (individuals, id);
+
+  return NULL;
+}
+
+void
+empathy_individual_manager_remove (EmpathyIndividualManager *self,
+    FolksIndividual *individual,
+    const gchar *message)
+{
+  /* TODO: implement */
+  DEBUG (G_STRLOC ": individual removal not implemented");
+}
diff --git a/libempathy/empathy-individual-manager.h b/libempathy/empathy-individual-manager.h
new file mode 100644 (file)
index 0000000..1823dec
--- /dev/null
@@ -0,0 +1,82 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2007-2010 Collabora Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Authors: Xavier Claessens <xclaesse@gmail.com>
+ *          Travis Reitter <travis.reitter@collabora.co.uk>
+ */
+
+#ifndef __EMPATHY_INDIVIDUAL_MANAGER_H__
+#define __EMPATHY_INDIVIDUAL_MANAGER_H__
+
+#include <glib.h>
+#include <folks/folks.h>
+
+#include "empathy-contact.h"
+
+G_BEGIN_DECLS
+#define EMPATHY_TYPE_INDIVIDUAL_MANAGER         (empathy_individual_manager_get_type ())
+#define EMPATHY_INDIVIDUAL_MANAGER(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), EMPATHY_TYPE_INDIVIDUAL_MANAGER, EmpathyIndividualManager))
+#define EMPATHY_INDIVIDUAL_MANAGER_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), EMPATHY_TYPE_INDIVIDUAL_MANAGER, EmpathyIndividualManagerClass))
+#define EMPATHY_IS_INDIVIDUAL_MANAGER(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), EMPATHY_TYPE_INDIVIDUAL_MANAGER))
+#define EMPATHY_IS_INDIVIDUAL_MANAGER_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), EMPATHY_TYPE_INDIVIDUAL_MANAGER))
+#define EMPATHY_INDIVIDUAL_MANAGER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), EMPATHY_TYPE_INDIVIDUAL_MANAGER, EmpathyIndividualManagerClass))
+    typedef enum
+{
+  EMPATHY_INDIVIDUAL_MANAGER_CAN_ADD = 1 << 0,
+  EMPATHY_INDIVIDUAL_MANAGER_CAN_REMOVE = 1 << 1,
+  EMPATHY_INDIVIDUAL_MANAGER_CAN_ALIAS = 1 << 2,
+  EMPATHY_INDIVIDUAL_MANAGER_CAN_GROUP = 1 << 3,
+} EmpathyIndividualManagerFlags;
+
+typedef struct _EmpathyIndividualManager EmpathyIndividualManager;
+typedef struct _EmpathyIndividualManagerClass EmpathyIndividualManagerClass;
+
+struct _EmpathyIndividualManager
+{
+  GObject parent;
+  gpointer priv;
+};
+
+struct _EmpathyIndividualManagerClass
+{
+  GObjectClass parent_class;
+};
+
+GType empathy_individual_manager_get_type (void) G_GNUC_CONST;
+
+gboolean empathy_individual_manager_initialized (void);
+
+EmpathyIndividualManager *empathy_individual_manager_dup_singleton (void);
+
+GList *empathy_individual_manager_get_members (
+    EmpathyIndividualManager *manager);
+
+FolksIndividual *empathy_individual_manager_lookup_member (
+    EmpathyIndividualManager *manager,
+    const gchar *id);
+
+void empathy_individual_manager_remove (EmpathyIndividualManager *manager,
+    FolksIndividual *individual,
+    const gchar *message);
+
+gboolean empathy_individual_manager_is_favourite (
+    EmpathyIndividualManager *manager,
+    FolksIndividual *individual);
+
+G_END_DECLS
+#endif /* __EMPATHY_INDIVIDUAL_MANAGER_H__ */
index 1c2d95b093bffb56af3942c71204eb2ff27fcc17..56da323b92b0a84a73fbdd7236524a4814f48c24 100644 (file)
@@ -32,6 +32,9 @@
 
 #include <libxml/uri.h>
 
+#include <folks/folks.h>
+#include <folks/folks-telepathy.h>
+
 #include <telepathy-glib/account-manager.h>
 #include <telepathy-glib/connection.h>
 #include <telepathy-glib/channel.h>
@@ -44,6 +47,7 @@
 #include "empathy-dispatch-operation.h"
 #include "empathy-idle.h"
 #include "empathy-tp-call.h"
+#include "empathy-tp-contact-factory.h"
 
 #include <extensions/extensions.h>
 
@@ -558,3 +562,34 @@ empathy_connect_new_account (TpAccount *account,
         break;
     }
 }
+
+TpConnectionPresenceType
+empathy_folks_presence_type_to_tp (FolksPresenceType type)
+{
+  return (TpConnectionPresenceType) type;
+}
+
+EmpathyContact *
+empathy_contact_from_folks_individual (FolksIndividual *individual)
+{
+  GList *personas, *l;
+  EmpathyContact *contact = NULL;
+
+  g_return_val_if_fail (FOLKS_IS_INDIVIDUAL (individual), NULL);
+
+  personas = folks_individual_get_personas (individual);
+  for (l = personas; (l != NULL) && (contact == NULL); l = l->next)
+    {
+      TpfPersona *persona = l->data;
+
+      if (TPF_IS_PERSONA (persona))
+        {
+          TpContact *tp_contact;
+
+          tp_contact = tpf_persona_get_contact (persona);
+          contact = empathy_contact_dup_from_tp_contact (tp_contact);
+        }
+    }
+
+  return contact;
+}
index 272b66fd8888245f63ff78f8e703361fab5d66ac..1ba7e53f81d81f4a9cbb21fd2e96388d1e44bcf0 100644 (file)
@@ -31,6 +31,7 @@
 
 #include <libxml/parser.h>
 #include <libxml/tree.h>
+#include <folks/folks.h>
 #include <telepathy-glib/account-manager.h>
 
 #include "empathy-contact.h"
@@ -89,6 +90,9 @@ gboolean empathy_account_manager_get_accounts_connected (gboolean *connecting);
 void empathy_connect_new_account (TpAccount *account,
     TpAccountManager *account_manager);
 
+TpConnectionPresenceType empathy_folks_presence_type_to_tp (FolksPresenceType type);
+EmpathyContact * empathy_contact_from_folks_individual (FolksIndividual *individual);
+
 G_END_DECLS
 
 #endif /*  __EMPATHY_UTILS_H__ */
index 4a32f8939980195fa70193b5be9dc2dffcbb2365..9117c1b62527cb395b19d9cb3a8bb14839a725ab 100644 (file)
@@ -30,6 +30,7 @@
 #include <glib/gi18n.h>
 
 #include <telepathy-glib/account-manager.h>
+#include <folks/folks.h>
 
 #include <libempathy/empathy-contact.h>
 #include <libempathy/empathy-idle.h>
@@ -40,7 +41,9 @@
 #include <libempathy/empathy-contact-list.h>
 #include <libempathy/empathy-contact-manager.h>
 #include <libempathy/empathy-gsettings.h>
+#include <libempathy/empathy-individual-manager.h>
 #include <libempathy/empathy-status-presets.h>
+#include <libempathy/empathy-tp-contact-factory.h>
 
 #include <libempathy-gtk/empathy-contact-dialogs.h>
 #include <libempathy-gtk/empathy-contact-list-store.h>
@@ -48,6 +51,8 @@
 #include <libempathy-gtk/empathy-live-search.h>
 #include <libempathy-gtk/empathy-geometry.h>
 #include <libempathy-gtk/empathy-gtk-enum-types.h>
+#include <libempathy-gtk/empathy-individual-store.h>
+#include <libempathy-gtk/empathy-individual-view.h>
 #include <libempathy-gtk/empathy-new-message-dialog.h>
 #include <libempathy-gtk/empathy-new-call-dialog.h>
 #include <libempathy-gtk/empathy-log-window.h>
@@ -88,8 +93,8 @@ G_DEFINE_TYPE (EmpathyMainWindow, empathy_main_window, GTK_TYPE_WINDOW);
 #define GET_PRIV(self) ((EmpathyMainWindowPriv *)((EmpathyMainWindow *) self)->priv)
 
 struct _EmpathyMainWindowPriv {
-       EmpathyContactListView  *list_view;
-       EmpathyContactListStore *list_store;
+       EmpathyIndividualStore  *individual_store;
+       EmpathyIndividualView   *individual_view;
        TpAccountManager        *account_manager;
        EmpathyChatroomManager  *chatroom_manager;
        EmpathyEventManager     *event_manager;
@@ -165,19 +170,19 @@ main_window_flash_foreach (GtkTreeModel *model,
                           gpointer      user_data)
 {
        FlashForeachData *data = (FlashForeachData *) user_data;
+       FolksIndividual *individual;
        EmpathyContact   *contact;
        const gchar      *icon_name;
        GtkTreePath      *parent_path = NULL;
        GtkTreeIter       parent_iter;
        GdkPixbuf        *pixbuf = NULL;
 
-       /* To be used with gtk_tree_model_foreach, update the status icon
-        * of the contact to show the event icon (on=TRUE) or the presence
-        * (on=FALSE) */
        gtk_tree_model_get (model, iter,
-                           EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
+                           EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL,
+                               &individual,
                            -1);
 
+       contact = empathy_contact_from_folks_individual (individual);
        if (contact != data->event->contact) {
                if (contact) {
                        g_object_unref (contact);
@@ -189,13 +194,13 @@ main_window_flash_foreach (GtkTreeModel *model,
                icon_name = data->event->icon_name;
                pixbuf = empathy_pixbuf_from_icon_name (icon_name, GTK_ICON_SIZE_MENU);
        } else {
-               pixbuf = contact_list_store_get_contact_status_icon (
-                                               GET_PRIV (data->window)->list_store,
-                                               contact);
+               pixbuf = empathy_individual_store_get_individual_status_icon (
+                                               GET_PRIV (data->window)->individual_store,
+                                               individual);
        }
 
        gtk_tree_store_set (GTK_TREE_STORE (model), iter,
-                           EMPATHY_CONTACT_LIST_STORE_COL_ICON_STATUS, pixbuf,
+                           EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS, pixbuf,
                            -1);
 
        /* To make sure the parent is shown correctly, we emit
@@ -210,7 +215,9 @@ main_window_flash_foreach (GtkTreeModel *model,
                gtk_tree_path_free (parent_path);
        }
 
-       g_object_unref (contact);
+       g_object_unref (individual);
+       if (contact)
+               g_object_unref (contact);
 
        return FALSE;
 }
@@ -226,7 +233,7 @@ main_window_flash_cb (EmpathyMainWindow *window)
 
        priv->flash_on = !priv->flash_on;
        data.on = priv->flash_on;
-       model = GTK_TREE_MODEL (priv->list_store);
+       model = GTK_TREE_MODEL (priv->individual_store);
 
        events = empathy_event_manager_get_events (priv->event_manager);
        for (l = events; l; l = l->next) {
@@ -290,7 +297,7 @@ main_window_event_removed_cb (EmpathyEventManager *manager,
        data.on = FALSE;
        data.event = event;
        data.window = window;
-       gtk_tree_model_foreach (GTK_TREE_MODEL (priv->list_store),
+       gtk_tree_model_foreach (GTK_TREE_MODEL (priv->individual_store),
                                main_window_flash_foreach,
                                &data);
 }
@@ -302,19 +309,26 @@ main_window_row_activated_cb (EmpathyContactListView *view,
                              EmpathyMainWindow      *window)
 {
        EmpathyMainWindowPriv *priv = GET_PRIV (window);
-       EmpathyContact *contact;
+       EmpathyContact *contact = NULL;
+       FolksIndividual *individual;
        GtkTreeModel   *model;
        GtkTreeIter     iter;
        GSList         *events, *l;
 
-       model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->list_view));
+       model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->individual_view));
        gtk_tree_model_get_iter (model, &iter, path);
+
        gtk_tree_model_get (model, &iter,
-                           EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
+                           EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL,
+                               &individual,
                            -1);
 
+       if (individual != NULL) {
+               contact = empathy_contact_from_folks_individual (individual);
+       }
+
        if (!contact) {
-               return;
+               goto OUT;
        }
 
        /* If the contact has an event activate it, otherwise the
@@ -335,6 +349,9 @@ main_window_row_activated_cb (EmpathyContactListView *view,
        }
 
        g_object_unref (contact);
+OUT:
+       if (individual)
+               g_object_unref (individual);
 }
 
 static void
@@ -617,7 +634,7 @@ empathy_main_window_finalize (GObject *window)
        g_list_free (priv->actions_connected);
 
        g_object_unref (priv->account_manager);
-       g_object_unref (priv->list_store);
+       g_object_unref (priv->individual_store);
        g_hash_table_destroy (priv->errors);
 
        /* disconnect all handlers of status-changed signal */
@@ -717,7 +734,8 @@ main_window_view_show_offline_cb (GtkToggleAction   *action,
 
        /* Turn off sound just while we alter the contact list. */
        // FIXME: empathy_sound_set_enabled (FALSE);
-       empathy_contact_list_store_set_show_offline (priv->list_store, current);
+       empathy_individual_store_set_show_offline (priv->individual_store,
+                       current);
        //empathy_sound_set_enabled (TRUE);
 }
 
@@ -736,7 +754,7 @@ main_window_notify_sort_contact_cb (GSettings         *gsettings,
                GEnumClass *enum_class;
                GEnumValue *enum_value;
 
-               type = empathy_contact_list_store_sort_get_type ();
+               type = empathy_individual_store_sort_get_type ();
                enum_class = G_ENUM_CLASS (g_type_class_peek (type));
                enum_value = g_enum_get_value_by_nick (enum_class, str);
                if (enum_value) {
@@ -768,7 +786,7 @@ main_window_view_sort_contacts_cb (GtkRadioAction    *action,
        group = gtk_radio_action_get_group (action);
 
        /* Get string from index */
-       type = empathy_contact_list_store_sort_get_type ();
+               type = empathy_individual_store_sort_get_type ();
        enum_class = G_ENUM_CLASS (g_type_class_peek (type));
        enum_value = g_enum_get_value (enum_class, g_slist_index (group, current));
 
@@ -780,7 +798,8 @@ main_window_view_sort_contacts_cb (GtkRadioAction    *action,
                                       EMPATHY_PREFS_CONTACTS_SORT_CRITERIUM,
                                       enum_value->value_nick);
        }
-       empathy_contact_list_store_set_sort_criterium (priv->list_store, value);
+       empathy_individual_store_set_sort_criterium (priv->individual_store,
+                       value);
 }
 
 static void
@@ -795,8 +814,8 @@ main_window_view_show_protocols_cb (GtkToggleAction   *action,
        g_settings_set_boolean (priv->gsettings_ui,
                                EMPATHY_PREFS_UI_SHOW_PROTOCOLS,
                                value);
-       empathy_contact_list_store_set_show_protocols (priv->list_store,
-                                                      value);
+       empathy_individual_store_set_show_protocols (priv->individual_store,
+                                                    value);
 }
 
 /* Matches GtkRadioAction values set in empathy-main-window.ui */
@@ -830,10 +849,11 @@ main_window_view_contacts_list_size_cb (GtkRadioAction    *action,
                                value == CONTACT_LIST_COMPACT_SIZE);
        g_settings_apply (gsettings_ui);
 
-       empathy_contact_list_store_set_show_avatars (priv->list_store,
-                                                    value == CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS);
-       empathy_contact_list_store_set_is_compact (priv->list_store,
-                                                  value == CONTACT_LIST_COMPACT_SIZE);
+       /* FIXME: these enums probably have the wrong namespace */
+       empathy_individual_store_set_show_avatars (priv->individual_store,
+                       value == CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS);
+       empathy_individual_store_set_is_compact (priv->individual_store,
+                       value == CONTACT_LIST_COMPACT_SIZE);
 
        g_object_unref (gsettings_ui);
 }
@@ -1144,7 +1164,8 @@ main_window_edit_cb (GtkAction         *action,
        GtkWidget *submenu;
 
        /* FIXME: It should use the UIManager to merge the contact/group submenu */
-       submenu = empathy_contact_list_view_get_contact_menu (priv->list_view);
+       submenu = empathy_individual_view_get_individual_menu (
+                       priv->individual_view);
        if (submenu) {
                GtkMenuItem *item;
                GtkWidget   *label;
@@ -1161,7 +1182,8 @@ main_window_edit_cb (GtkAction         *action,
                return;
        }
 
-       submenu = empathy_contact_list_view_get_group_menu (priv->list_view);
+       submenu = empathy_individual_view_get_group_menu (
+                       priv->individual_view);
        if (submenu) {
                GtkMenuItem *item;
                GtkWidget   *label;
@@ -1458,6 +1480,7 @@ empathy_main_window_init (EmpathyMainWindow *window)
 {
        EmpathyMainWindowPriv    *priv;
        EmpathyContactList       *list_iface;
+       EmpathyIndividualManager *individual_manager;
        GtkBuilder               *gui;
        GtkWidget                *sw;
        GtkToggleAction          *show_offline_widget;
@@ -1595,33 +1618,36 @@ empathy_main_window_init (EmpathyMainWindow *window)
        priv->throbber_tool_item = GTK_WIDGET (item);
 
        list_iface = EMPATHY_CONTACT_LIST (empathy_contact_manager_dup_singleton ());
-       priv->list_store = empathy_contact_list_store_new (list_iface);
-       priv->list_view = empathy_contact_list_view_new (priv->list_store,
-                                                        EMPATHY_CONTACT_LIST_FEATURE_ALL,
-                                                        EMPATHY_CONTACT_FEATURE_ALL);
+       individual_manager = empathy_individual_manager_dup_singleton ();
+       priv->individual_store = empathy_individual_store_new (
+                       individual_manager);
+       priv->individual_view = empathy_individual_view_new (
+                       priv->individual_store,
+                       EMPATHY_INDIVIDUAL_VIEW_FEATURE_ALL,
+                       EMPATHY_INDIVIDUAL_FEATURE_ALL);
 
        priv->butterfly_log_migration_members_changed_id = g_signal_connect (
-               list_iface, "members-changed",
-               G_CALLBACK (main_window_members_changed_cb), window);
+                       list_iface, "members-changed",
+                       G_CALLBACK (main_window_members_changed_cb), window);
 
        g_object_unref (list_iface);
 
-       gtk_widget_show (GTK_WIDGET (priv->list_view));
+       gtk_widget_show (GTK_WIDGET (priv->individual_view));
        gtk_container_add (GTK_CONTAINER (sw),
-                          GTK_WIDGET (priv->list_view));
-       g_signal_connect (priv->list_view, "row-activated",
+                          GTK_WIDGET (priv->individual_view));
+       g_signal_connect (priv->individual_view, "row-activated",
                          G_CALLBACK (main_window_row_activated_cb),
                          window);
 
        /* Set up search bar */
        priv->search_bar = empathy_live_search_new (
-               GTK_WIDGET (priv->list_view));
-       empathy_contact_list_view_set_live_search (priv->list_view,
+               GTK_WIDGET (priv->individual_view));
+       empathy_individual_view_set_live_search (priv->individual_view,
                EMPATHY_LIVE_SEARCH (priv->search_bar));
        gtk_box_pack_start (GTK_BOX (priv->main_vbox), priv->search_bar,
                FALSE, TRUE, 0);
        g_signal_connect_swapped (window, "map",
-               G_CALLBACK (gtk_widget_grab_focus), priv->list_view);
+               G_CALLBACK (gtk_widget_grab_focus), priv->individual_view);
 
        /* Load user-defined accelerators. */
        main_window_accels_load ();
@@ -1631,13 +1657,11 @@ empathy_main_window_init (EmpathyMainWindow *window)
 
        /* Enable event handling */
        priv->event_manager = empathy_event_manager_dup_singleton ();
+
        g_signal_connect (priv->event_manager, "event-added",
-                         G_CALLBACK (main_window_event_added_cb),
-                         window);
+                         G_CALLBACK (main_window_event_added_cb), window);
        g_signal_connect (priv->event_manager, "event-removed",
-                         G_CALLBACK (main_window_event_removed_cb),
-                         window);
-
+                         G_CALLBACK (main_window_event_removed_cb), window);
        g_signal_connect (priv->account_manager, "account-validity-changed",
                          G_CALLBACK (main_window_account_validity_changed_cb),
                          window);
@@ -1650,8 +1674,8 @@ empathy_main_window_init (EmpathyMainWindow *window)
 
        l = empathy_event_manager_get_events (priv->event_manager);
        while (l) {
-               main_window_event_added_cb (priv->event_manager,
-                                           l->data, window);
+               main_window_event_added_cb (priv->event_manager, l->data,
+                               window);
                l = l->next;
        }