]> git.0d.be Git - empathy.git/blobdiff - libempathy-gtk/empathy-individual-view.c
individual_view_drag_end: remove the auto scroll
[empathy.git] / libempathy-gtk / empathy-individual-view.c
index 6f8cda08e17b65d03b81f20da926f40c4e112bcb..90618997d36e1295c2f388681b341a235fd24315 100644 (file)
 #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 <folks/folks.h>
+#include <folks/folks-telepathy.h>
+
 #include <libempathy/empathy-individual-manager.h>
 #include <libempathy/empathy-contact-groups.h>
-#include <libempathy/empathy-dispatcher.h>
+#include <libempathy/empathy-request-util.h>
 #include <libempathy/empathy-utils.h>
 
 #include "empathy-individual-view.h"
 #include "empathy-individual-menu.h"
 #include "empathy-individual-store.h"
+#include "empathy-individual-edit-dialog.h"
+#include "empathy-individual-dialogs.h"
 #include "empathy-images.h"
+#include "empathy-linking-dialog.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>
@@ -66,14 +69,28 @@ typedef struct
   EmpathyIndividualStore *store;
   GtkTreeRowReference *drag_row;
   EmpathyIndividualViewFeatureFlags view_features;
-  EmpathyContactFeatureFlags individual_features;
+  EmpathyIndividualFeatureFlags individual_features;
   GtkWidget *tooltip_widget;
-  GtkTargetList *file_targets;
 
   gboolean show_offline;
+  gboolean show_untrusted;
+  gboolean show_uninteresting;
 
   GtkTreeModelFilter *filter;
   GtkWidget *search_widget;
+
+  guint expand_groups_idle_handler;
+  /* owned string (group name) -> bool (whether to expand/contract) */
+  GHashTable *expand_groups;
+
+  /* Auto scroll */
+  guint auto_scroll_timeout_id;
+  /* Distance between mouse pointer and the nearby border. Negative when
+     scrolling updards.*/
+  gint distance;
+
+  GtkTreeModelFilterVisibleFunc custom_filter;
+  gpointer custom_filter_data;
 } EmpathyIndividualViewPriv;
 
 typedef struct
@@ -97,45 +114,45 @@ enum
   PROP_VIEW_FEATURES,
   PROP_INDIVIDUAL_FEATURES,
   PROP_SHOW_OFFLINE,
+  PROP_SHOW_UNTRUSTED,
+  PROP_SHOW_UNINTERESTING,
 };
 
 /* TODO: re-add DRAG_TYPE_CONTACT_ID, for the case that we're dragging around
  * specific EmpathyContacts (between/in/out of Individuals) */
-enum DndDragType
+typedef enum
 {
-  DND_DRAG_TYPE_INDIVIDUAL_ID,
+  DND_DRAG_TYPE_UNKNOWN = -1,
+  DND_DRAG_TYPE_INDIVIDUAL_ID = 0,
+  DND_DRAG_TYPE_PERSONA_ID,
   DND_DRAG_TYPE_URI_LIST,
   DND_DRAG_TYPE_STRING,
-};
+} DndDragType;
 
 #define DRAG_TYPE(T,I) \
   { (gchar *) T, 0, I }
 
 static const GtkTargetEntry drag_types_dest[] = {
+  DRAG_TYPE ("text/x-individual-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
+  DRAG_TYPE ("text/x-persona-id", DND_DRAG_TYPE_PERSONA_ID),
   DRAG_TYPE ("text/path-list", DND_DRAG_TYPE_URI_LIST),
   DRAG_TYPE ("text/uri-list", DND_DRAG_TYPE_URI_LIST),
-  DRAG_TYPE ("text/contact-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
   DRAG_TYPE ("text/plain", DND_DRAG_TYPE_STRING),
   DRAG_TYPE ("STRING", DND_DRAG_TYPE_STRING),
 };
 
-static const GtkTargetEntry drag_types_dest_file[] = {
-  DRAG_TYPE ("text/path-list", DND_DRAG_TYPE_URI_LIST),
-  DRAG_TYPE ("text/uri-list", DND_DRAG_TYPE_URI_LIST),
-};
-
 static const GtkTargetEntry drag_types_source[] = {
-  DRAG_TYPE ("text/contact-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
+  DRAG_TYPE ("text/x-individual-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
 };
 
 #undef DRAG_TYPE
 
 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,
+  DRAG_INDIVIDUAL_RECEIVED,
+  DRAG_PERSONA_RECEIVED,
   LAST_SIGNAL
 };
 
@@ -150,11 +167,7 @@ individual_view_tooltip_destroy_cb (GtkWidget *widget,
 {
   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
 
-  if (priv->tooltip_widget != NULL)
-    {
-      DEBUG ("Tooltip destroyed");
-      tp_clear_object (&priv->tooltip_widget);
-    }
+  tp_clear_object (&priv->tooltip_widget);
 }
 
 static gboolean
@@ -172,7 +185,6 @@ individual_view_query_tooltip_cb (EmpathyIndividualView *view,
   GtkTreePath *path;
   static gint running = 0;
   gboolean ret = FALSE;
-  EmpathyContact *contact;
 
   priv = GET_PRIV (view);
 
@@ -199,30 +211,30 @@ individual_view_query_tooltip_cb (EmpathyIndividualView *view,
   if (individual == NULL)
     goto OUT;
 
-  contact = empathy_contact_dup_from_folks_individual (individual);
-  g_object_unref (individual);
-
-  if (contact == NULL)
-    goto OUT;
-
   if (priv->tooltip_widget == NULL)
     {
-      priv->tooltip_widget = empathy_contact_widget_new (contact,
-          EMPATHY_CONTACT_WIDGET_FOR_TOOLTIP |
-          EMPATHY_CONTACT_WIDGET_SHOW_LOCATION);
+      priv->tooltip_widget = empathy_individual_widget_new (individual,
+          EMPATHY_INDIVIDUAL_WIDGET_FOR_TOOLTIP |
+          EMPATHY_INDIVIDUAL_WIDGET_SHOW_LOCATION |
+          EMPATHY_INDIVIDUAL_WIDGET_SHOW_CLIENT_TYPES);
       gtk_container_set_border_width (GTK_CONTAINER (priv->tooltip_widget), 8);
       g_object_ref (priv->tooltip_widget);
-      g_signal_connect (priv->tooltip_widget, "destroy",
-          G_CALLBACK (individual_view_tooltip_destroy_cb), view);
+
+      tp_g_signal_connect_object (priv->tooltip_widget, "destroy",
+          G_CALLBACK (individual_view_tooltip_destroy_cb), view, 0);
+
       gtk_widget_show (priv->tooltip_widget);
     }
   else
-    empathy_contact_widget_set_contact (priv->tooltip_widget, contact);
+    {
+      empathy_individual_widget_set_individual (
+        EMPATHY_INDIVIDUAL_WIDGET (priv->tooltip_widget), individual);
+    }
 
   gtk_tooltip_set_custom (tooltip, priv->tooltip_widget);
   ret = TRUE;
 
-  g_object_unref (contact);
+  g_object_unref (individual);
 OUT:
   running--;
 
@@ -234,10 +246,10 @@ groups_change_group_cb (GObject *source,
     GAsyncResult *result,
     gpointer user_data)
 {
-  FolksGroups *groups = FOLKS_GROUPS (source);
+  FolksGroupDetails *group_details = FOLKS_GROUP_DETAILS (source);
   GError *error = NULL;
 
-  folks_groups_change_group_finish (groups, result, &error);
+  folks_group_details_change_group_finish (group_details, result, &error);
   if (error != NULL)
     {
       g_warning ("failed to change group: %s", error->message);
@@ -245,44 +257,6 @@ groups_change_group_cb (GObject *source,
     }
 }
 
-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 */
-      folks_favourite_set_is_favourite (FOLKS_FAVOURITE (individual), TRUE);
-      return;
-    }
-
-  if (!tp_strdiff (old_group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
-    {
-      /* Remove contact as favourite */
-      folks_favourite_set_is_favourite (FOLKS_FAVOURITE (individual), FALSE);
-
-      /* Don't try to remove it */
-      old_group = NULL;
-    }
-
-  if (new_group != NULL)
-    folks_groups_change_group (FOLKS_GROUPS (individual), new_group, TRUE,
-        groups_change_group_cb, NULL);
-
-  if (old_group != NULL && action == GDK_ACTION_MOVE)
-    folks_groups_change_group (FOLKS_GROUPS (individual), old_group, FALSE,
-        groups_change_group_cb, NULL);
-}
-
 static gboolean
 group_can_be_modified (const gchar *name,
     gboolean is_fake_group,
@@ -305,20 +279,20 @@ group_can_be_modified (const gchar *name,
 }
 
 static gboolean
-individual_view_contact_drag_received (GtkWidget *self,
+individual_view_individual_drag_received (GtkWidget *self,
     GdkDragContext *context,
     GtkTreeModel *model,
     GtkTreePath *path,
     GtkSelectionData *selection)
 {
   EmpathyIndividualViewPriv *priv;
-  EmpathyIndividualManager *manager;
+  EmpathyIndividualManager *manager = NULL;
   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;
+  gboolean new_group_is_fake, old_group_is_fake = TRUE, retval = FALSE;
 
   priv = GET_PRIV (self);
 
@@ -327,10 +301,16 @@ individual_view_contact_drag_received (GtkWidget *self,
       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)
+    goto finished;
+
+  /* Get source group information iff the view has the FEATURE_GROUPS_CHANGE
+   * feature. Otherwise, we just add the dropped contact to whichever group
+   * they were dropped in, and don't remove them from their old group. This
+   * allows for Individual views which shouldn't allow Individuals to have
+   * their groups changed, and also for dragging Individuals between Individual
+   * views. */
+  if ((priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_CHANGE) &&
+      priv->drag_row != NULL)
     {
       source_path = gtk_tree_row_reference_get_path (priv->drag_row);
       if (source_path)
@@ -340,16 +320,19 @@ individual_view_contact_drag_received (GtkWidget *self,
               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 (!group_can_be_modified (old_group, old_group_is_fake, FALSE))
+        goto finished;
 
-  if (!tp_strdiff (old_group, new_group))
+      if (!tp_strdiff (old_group, new_group))
+        goto finished;
+    }
+  else if (priv->drag_row != NULL)
     {
-      g_free (new_group);
-      g_free (old_group);
-      return FALSE;
+      /* We don't allow changing Individuals' groups, and this Individual was
+       * dragged from another group in *this* Individual view, so we disallow
+       * the drop. */
+      goto finished;
     }
 
   /* XXX: for contacts, we used to ensure the account, create the contact
@@ -362,23 +345,135 @@ individual_view_contact_drag_received (GtkWidget *self,
   if (individual == NULL)
     {
       DEBUG ("failed to find drag event individual with ID '%s'", sel_data);
-
-      g_object_unref (manager);
-
-      return FALSE;
+      goto finished;
     }
 
   /* 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));
+  /* Emit a signal notifying of the drag. We change the Individual's groups in
+   * the default signal handler. */
+  g_signal_emit (self, signals[DRAG_INDIVIDUAL_RECEIVED], 0,
+      gdk_drag_context_get_selected_action (context), individual, new_group,
+      old_group);
+
+  retval = TRUE;
 
-  g_object_unref (G_OBJECT (manager));
+finished:
+  tp_clear_object (&manager);
   g_free (old_group);
   g_free (new_group);
 
-  return TRUE;
+  return retval;
+}
+
+static void
+real_drag_individual_received_cb (EmpathyIndividualView *self,
+    GdkDragAction action,
+    FolksIndividual *individual,
+    const gchar *new_group,
+    const gchar *old_group)
+{
+  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 */
+      folks_favourite_details_set_is_favourite (
+          FOLKS_FAVOURITE_DETAILS (individual), TRUE);
+      return;
+    }
+
+  if (!tp_strdiff (old_group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
+    {
+      /* Remove contact as favourite */
+      folks_favourite_details_set_is_favourite (
+          FOLKS_FAVOURITE_DETAILS (individual), FALSE);
+
+      /* Don't try to remove it */
+      old_group = NULL;
+    }
+
+  if (new_group != NULL)
+    {
+      folks_group_details_change_group (FOLKS_GROUP_DETAILS (individual),
+          new_group, TRUE, groups_change_group_cb, NULL);
+    }
+
+  if (old_group != NULL && action == GDK_ACTION_MOVE)
+    {
+      folks_group_details_change_group (FOLKS_GROUP_DETAILS (individual),
+          old_group, FALSE, groups_change_group_cb, NULL);
+    }
+}
+
+static gboolean
+individual_view_persona_drag_received (GtkWidget *self,
+    GdkDragContext *context,
+    GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkSelectionData *selection)
+{
+  EmpathyIndividualManager *manager = NULL;
+  FolksIndividual *individual = NULL;
+  FolksPersona *persona = NULL;
+  const gchar *persona_uid;
+  GList *individuals, *l;
+  GeeIterator *iter = NULL;
+  gboolean retval = FALSE;
+
+  persona_uid = (const gchar *) gtk_selection_data_get_data (selection);
+
+  /* FIXME: This is slow, but the only way to find the Persona we're having
+   * dropped on us. */
+  manager = empathy_individual_manager_dup_singleton ();
+  individuals = empathy_individual_manager_get_members (manager);
+
+  for (l = individuals; l != NULL; l = l->next)
+    {
+      GeeSet *personas;
+
+      personas = folks_individual_get_personas (FOLKS_INDIVIDUAL (l->data));
+      iter = gee_iterable_iterator (GEE_ITERABLE (personas));
+      while (gee_iterator_next (iter))
+        {
+          FolksPersona *persona_cur = gee_iterator_get (iter);
+
+          if (!tp_strdiff (folks_persona_get_uid (persona), persona_uid))
+            {
+              /* takes ownership of the ref */
+              persona = persona_cur;
+              individual = g_object_ref (l->data);
+              goto got_persona;
+            }
+          g_clear_object (&persona_cur);
+        }
+      g_clear_object (&iter);
+    }
+
+got_persona:
+  g_clear_object (&iter);
+  g_list_free (individuals);
+
+  if (persona == NULL || individual == NULL)
+    {
+      DEBUG ("Failed to find drag event persona with UID '%s'", persona_uid);
+    }
+  else
+    {
+      /* Emit a signal notifying of the drag. We change the Individual's groups in
+       * the default signal handler. */
+      g_signal_emit (self, signals[DRAG_PERSONA_RECEIVED], 0,
+          gdk_drag_context_get_selected_action (context), persona, individual,
+          &retval);
+    }
+
+  tp_clear_object (&manager);
+  tp_clear_object (&persona);
+  tp_clear_object (&individual);
+
+  return retval;
 }
 
 static gboolean
@@ -434,13 +529,17 @@ individual_view_drag_data_received (GtkWidget *view,
     {
       success = FALSE;
     }
-  else if (info == DND_DRAG_TYPE_INDIVIDUAL_ID
-      || info == DND_DRAG_TYPE_STRING)
+  else if (info == DND_DRAG_TYPE_INDIVIDUAL_ID)
     {
-      success = individual_view_contact_drag_received (view,
+      success = individual_view_individual_drag_received (view,
           context, model, path, selection);
     }
-  else if (info == DND_DRAG_TYPE_URI_LIST)
+  else if (info == DND_DRAG_TYPE_PERSONA_ID)
+    {
+      success = individual_view_persona_drag_received (view, context, model,
+          path, selection);
+    }
+  else if (info == DND_DRAG_TYPE_URI_LIST || info == DND_DRAG_TYPE_STRING)
     {
       success = individual_view_file_drag_received (view,
           context, model, path, selection);
@@ -453,13 +552,46 @@ individual_view_drag_data_received (GtkWidget *view,
 static gboolean
 individual_view_drag_motion_cb (DragMotionData *data)
 {
-  gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view), data->path, FALSE);
+  if (data->view != NULL)
+    {
+      gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view), data->path, FALSE);
+      g_object_remove_weak_pointer (G_OBJECT (data->view),
+          (gpointer *) &data->view);
+    }
 
   data->timeout_id = 0;
 
   return FALSE;
 }
 
+/* Minimum distance between the mouse pointer and a horizontal border when we
+   start auto scrolling. */
+#define AUTO_SCROLL_MARGIN_SIZE 20
+/* How far to scroll per one tick. */
+#define AUTO_SCROLL_PITCH       10
+
+static gboolean
+individual_view_auto_scroll_cb (EmpathyIndividualView *self)
+{
+       EmpathyIndividualViewPriv *priv = GET_PRIV (self);
+       GtkAdjustment         *adj;
+       gdouble                new_value;
+
+       adj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (self));
+
+       if (priv->distance < 0)
+               new_value = gtk_adjustment_get_value (adj) - AUTO_SCROLL_PITCH;
+       else
+               new_value = gtk_adjustment_get_value (adj) + AUTO_SCROLL_PITCH;
+
+       new_value = CLAMP (new_value, gtk_adjustment_get_lower (adj),
+               gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj));
+
+       gtk_adjustment_set_value (adj, new_value);
+
+       return TRUE;
+}
+
 static gboolean
 individual_view_drag_motion (GtkWidget *widget,
     GdkDragContext *context,
@@ -477,10 +609,34 @@ individual_view_drag_motion (GtkWidget *widget,
   gboolean is_different = FALSE;
   gboolean cleanup = TRUE;
   gboolean retval = TRUE;
+  GtkAllocation allocation;
+  guint i;
+  DndDragType drag_type = DND_DRAG_TYPE_UNKNOWN;
 
   priv = GET_PRIV (EMPATHY_INDIVIDUAL_VIEW (widget));
   model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
 
+
+  if (priv->auto_scroll_timeout_id != 0)
+    {
+      g_source_remove (priv->auto_scroll_timeout_id);
+      priv->auto_scroll_timeout_id = 0;
+    }
+
+  gtk_widget_get_allocation (widget, &allocation);
+
+  if (y < AUTO_SCROLL_MARGIN_SIZE ||
+      y > (allocation.height - AUTO_SCROLL_MARGIN_SIZE))
+    {
+      if (y < AUTO_SCROLL_MARGIN_SIZE)
+        priv->distance = MIN (-y, -1);
+      else
+        priv->distance = MAX (allocation.height - y, 1);
+
+      priv->auto_scroll_timeout_id = g_timeout_add (10 * ABS (priv->distance),
+          (GSourceFunc) individual_view_auto_scroll_cb, widget);
+    }
+
   is_row = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget),
       x, y, &path, NULL, NULL, NULL);
 
@@ -504,17 +660,85 @@ individual_view_drag_motion (GtkWidget *widget,
       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);
+  target = gtk_drag_dest_find_target (widget, context, NULL);
   gtk_tree_model_get_iter (model, &iter, path);
 
-  if (target == GDK_NONE)
+  /* Determine the DndDragType of the data */
+  for (i = 0; i < G_N_ELEMENTS (drag_atoms_dest); i++)
+    {
+      if (target == drag_atoms_dest[i])
+        {
+          drag_type = drag_types_dest[i].info;
+          break;
+        }
+    }
+
+  if (drag_type == DND_DRAG_TYPE_URI_LIST ||
+      drag_type == DND_DRAG_TYPE_STRING)
+    {
+      /* This is a file drag, and it can only be dropped on contacts,
+       * not groups.
+       * If we don't have FEATURE_FILE_DROP, disallow the drop completely,
+       * even if we have a valid target. */
+      FolksIndividual *individual = NULL;
+      EmpathyCapabilities caps = EMPATHY_CAPABILITIES_NONE;
+
+      if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_FILE_DROP)
+        {
+          gtk_tree_model_get (model, &iter,
+              EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
+              -1);
+        }
+
+      if (individual != NULL)
+        {
+          EmpathyContact *contact = NULL;
+
+          contact = empathy_contact_dup_from_folks_individual (individual);
+          if (contact != NULL)
+            caps = empathy_contact_get_capabilities (contact);
+
+          tp_clear_object (&contact);
+        }
+
+      if (individual != NULL &&
+          folks_presence_details_is_online (
+              FOLKS_PRESENCE_DETAILS (individual)) &&
+          (caps & EMPATHY_CAPABILITIES_FT))
+        {
+          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);
+        }
+      else
+        {
+          gdk_drag_status (context, 0, time_);
+          gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
+          retval = FALSE;
+        }
+
+      if (individual != NULL)
+        g_object_unref (individual);
+    }
+  else if ((drag_type == DND_DRAG_TYPE_INDIVIDUAL_ID &&
+      (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_CHANGE ||
+       priv->drag_row == NULL)) ||
+      (drag_type == DND_DRAG_TYPE_PERSONA_ID &&
+       priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_PERSONA_DROP))
     {
-      /* 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,
+      /* If target != GDK_NONE, then we have a contact (individual or persona)
+         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.
+         If it's an Individual:
+           We only highlight things if the contact is from a different
+           Individual view, or if this Individual view has
+           FEATURE_GROUPS_CHANGE. This prevents highlighting in Individual views
+           which don't have FEATURE_GROUPS_CHANGE, but do have
+           FEATURE_INDIVIDUAL_DRAG and FEATURE_INDIVIDUAL_DROP.
+         If it's a Persona:
+           We only highlight things if we have FEATURE_PERSONA_DROP.
        */
       GtkTreeIter group_iter;
       gboolean is_group;
@@ -547,44 +771,6 @@ individual_view_drag_motion (GtkWidget *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;
-      EmpathyCapabilities caps = EMPATHY_CAPABILITIES_NONE;
-
-      gtk_tree_model_get (model, &iter,
-          EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
-      if (individual != NULL)
-        {
-          EmpathyContact *contact = NULL;
-
-          contact = empathy_contact_dup_from_folks_individual (individual);
-          caps = empathy_contact_get_capabilities (contact);
-
-          tp_clear_object (&contact);
-        }
-
-      if (individual != NULL &&
-          folks_individual_is_online (individual) &&
-          (caps & EMPATHY_CAPABILITIES_FT))
-        {
-          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);
-        }
-      else
-        {
-          gdk_drag_status (context, 0, time_);
-          gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
-          retval = FALSE;
-        }
-
-      if (individual != NULL)
-        g_object_unref (individual);
-    }
 
   if (!is_different && !cleanup)
     return retval;
@@ -607,6 +793,7 @@ individual_view_drag_motion (GtkWidget *widget,
       dm = g_new0 (DragMotionData, 1);
 
       dm->view = EMPATHY_INDIVIDUAL_VIEW (widget);
+      g_object_add_weak_pointer (G_OBJECT (widget), (gpointer *) &dm->view);
       dm->path = gtk_tree_path_copy (path);
 
       dm->timeout_id = g_timeout_add_seconds (1,
@@ -628,13 +815,13 @@ individual_view_drag_begin (GtkWidget *widget,
 
   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;
 
+  GTK_WIDGET_CLASS (empathy_individual_view_parent_class)->drag_begin (widget,
+      context);
+
   path = gtk_tree_model_get_path (model, &iter);
   priv->drag_row = gtk_tree_row_reference_new (model, path);
   gtk_tree_path_free (path);
@@ -681,7 +868,8 @@ individual_view_drag_data_get (GtkWidget *widget,
 
   if (info == DND_DRAG_TYPE_INDIVIDUAL_ID)
     {
-      gtk_selection_data_set (selection, drag_atoms_source[info], 8,
+      gtk_selection_data_set (selection,
+          gdk_atom_intern ("text/x-individual-id", FALSE), 8,
           (guchar *) individual_id, strlen (individual_id) + 1);
     }
 
@@ -704,6 +892,12 @@ individual_view_drag_end (GtkWidget *widget,
       gtk_tree_row_reference_free (priv->drag_row);
       priv->drag_row = NULL;
     }
+
+  if (priv->auto_scroll_timeout_id != 0)
+    {
+      g_source_remove (priv->auto_scroll_timeout_id);
+      priv->auto_scroll_timeout_id = 0;
+    }
 }
 
 static gboolean
@@ -723,6 +917,17 @@ typedef struct
   guint32 time;
 } MenuPopupData;
 
+static void
+menu_deactivate_cb (GtkMenuShell *menushell,
+    gpointer user_data)
+{
+  /* FIXME: we shouldn't have to disconnec the signal (bgo #641327) */
+  g_signal_handlers_disconnect_by_func (menushell,
+      menu_deactivate_cb, user_data);
+
+  gtk_menu_detach (GTK_MENU (menushell));
+}
+
 static gboolean
 individual_view_popup_menu_idle_cb (gpointer user_data)
 {
@@ -735,14 +940,20 @@ individual_view_popup_menu_idle_cb (gpointer user_data)
 
   if (menu != NULL)
     {
-      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);
+
+      /* menu is initially unowned but gtk_menu_attach_to_widget() taked its
+       * floating ref. We can either wait that the treeview releases its ref
+       * when it will be destroyed (when leaving Empathy) or explicitely
+       * detach the menu when it's not displayed any more.
+       * We go for the latter as we don't want to keep useless menus in memory
+       * during the whole lifetime of Empathy. */
+      g_signal_connect (menu, "deactivate", G_CALLBACK (menu_deactivate_cb),
+          NULL);
     }
 
   g_slice_free (MenuPopupData, data);
@@ -774,7 +985,7 @@ individual_view_key_press_event_cb (EmpathyIndividualView *view,
     GdkEventKey *event,
     gpointer user_data)
 {
-  if (event->keyval == GDK_Menu)
+  if (event->keyval == GDK_KEY_Menu)
     {
       MenuPopupData *data;
 
@@ -783,6 +994,18 @@ individual_view_key_press_event_cb (EmpathyIndividualView *view,
       data->button = 0;
       data->time = event->time;
       g_idle_add (individual_view_popup_menu_idle_cb, data);
+    } else if (event->keyval == GDK_KEY_F2) {
+        FolksIndividual *individual;
+
+        g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), FALSE);
+
+        individual = empathy_individual_view_dup_selected (view);
+        if (individual == NULL)
+            return FALSE;
+
+        empathy_individual_edit_dialog_show (individual, NULL);
+
+        g_object_unref (individual);
     }
 
   return FALSE;
@@ -795,11 +1018,11 @@ individual_view_row_activated (GtkTreeView *view,
 {
   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
   FolksIndividual *individual;
-  EmpathyContact *contact = NULL;
+  EmpathyContact *contact;
   GtkTreeModel *model;
   GtkTreeIter iter;
 
-  if (!(priv->individual_features & EMPATHY_CONTACT_FEATURE_CHAT))
+  if (!(priv->individual_features & EMPATHY_INDIVIDUAL_FEATURE_CHAT))
     return;
 
   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
@@ -810,12 +1033,15 @@ individual_view_row_activated (GtkTreeView *view,
   if (individual == NULL)
     return;
 
-  contact = empathy_contact_dup_from_folks_individual (individual);
+  /* Determine which Persona to chat to, by choosing the most available one. */
+  contact = empathy_contact_dup_best_for_action (individual,
+      EMPATHY_ACTION_CHAT);
+
   if (contact != NULL)
     {
       DEBUG ("Starting a chat");
 
-      empathy_dispatcher_chat_with_contact (contact,
+      empathy_chat_with_contact (contact,
           gtk_get_current_event_time ());
     }
 
@@ -828,6 +1054,7 @@ individual_view_call_activated_cb (EmpathyCellRendererActivatable *cell,
     const gchar *path_string,
     EmpathyIndividualView *view)
 {
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
   GtkWidget *menu;
   GtkTreeModel *model;
   GtkTreeIter iter;
@@ -836,6 +1063,9 @@ individual_view_call_activated_cb (EmpathyCellRendererActivatable *cell,
   GtkMenuShell *shell;
   GtkWidget *item;
 
+  if (!(priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_CALL))
+    return;
+
   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
   if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string))
     return;
@@ -847,26 +1077,22 @@ individual_view_call_activated_cb (EmpathyCellRendererActivatable *cell,
 
   event = (GdkEventButton *) gtk_get_current_event ();
 
-  menu = gtk_menu_new ();
+  menu = empathy_context_menu_new (GTK_WIDGET (view));
   shell = GTK_MENU_SHELL (menu);
 
   /* audio */
-  item = empathy_individual_audio_call_menu_item_new (individual, NULL);
+  item = empathy_individual_audio_call_menu_item_new (individual);
   gtk_menu_shell_append (shell, item);
   gtk_widget_show (item);
 
   /* video */
-  item = empathy_individual_video_call_menu_item_new (individual, NULL);
+  item = empathy_individual_video_call_menu_item_new (individual);
   gtk_menu_shell_append (shell, item);
   gtk_widget_show (item);
 
-  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);
 }
@@ -877,28 +1103,27 @@ individual_view_cell_set_background (EmpathyIndividualView *view,
     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];
+      GtkStyleContext *style;
+      GdkRGBA color;
+
+      style = gtk_widget_get_style_context (GTK_WIDGET (view));
+
+      gtk_style_context_get_background_color (style, GTK_STATE_FLAG_SELECTED,
+          &color);
 
       /* 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;
+      empathy_make_color_whiter (&color);
 
-      g_object_set (cell, "cell-background-gdk", &color, NULL);
+      g_object_set (cell, "cell-background-rgba", &color, NULL);
     }
   else
-    g_object_set (cell, "cell-background-gdk", NULL, NULL);
+    g_object_set (cell, "cell-background-rgba", NULL, NULL);
 }
 
 static void
@@ -1108,10 +1333,7 @@ individual_view_start_search_cb (EmpathyIndividualView *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));
+  empathy_individual_view_start_search (view);
 
   return TRUE;
 }
@@ -1196,20 +1418,15 @@ individual_view_search_key_navigation_cb (GtkWidget *search,
   GdkEvent *event,
   EmpathyIndividualView *view)
 {
-  GdkEventKey *eventkey = ((GdkEventKey *) event);
+  GdkEvent *new_event;
   gboolean ret = FALSE;
 
-  if (eventkey->keyval == GDK_Up || eventkey->keyval == GDK_Down)
-    {
-      GdkEvent *new_event;
-
-      new_event = gdk_event_copy (event);
-      gtk_widget_grab_focus (GTK_WIDGET (view));
-      ret = gtk_widget_event (GTK_WIDGET (view), new_event);
-      gtk_widget_grab_focus (search);
+  new_event = gdk_event_copy (event);
+  gtk_widget_grab_focus (GTK_WIDGET (view));
+  ret = gtk_widget_event (GTK_WIDGET (view), new_event);
+  gtk_widget_grab_focus (search);
 
-      gdk_event_free (new_event);
-    }
+  gdk_event_free (new_event);
 
   return ret;
 }
@@ -1298,41 +1515,80 @@ individual_view_search_show_cb (EmpathyLiveSearch *search,
       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)
+expand_idle_foreach_cb (GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkTreeIter *iter,
+    EmpathyIndividualView *self)
 {
-  ExpandData *data = user_data;
-  GtkTreePath *path;
+  EmpathyIndividualViewPriv *priv;
+  gboolean is_group;
+  gpointer should_expand;
+  gchar *name;
 
-  path = gtk_tree_row_reference_get_path (data->row_ref);
-  if (path == NULL)
-    goto done;
+  /* We only want groups */
+  if (gtk_tree_path_get_depth (path) > 1)
+    return FALSE;
 
-  g_signal_handlers_block_by_func (data->view,
-    individual_view_row_expand_or_collapse_cb,
-    GINT_TO_POINTER (data->expand));
+  gtk_tree_model_get (model, iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
+      -1);
 
-  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);
+  if (!is_group)
+    {
+      g_free (name);
+      return FALSE;
+    }
 
-  gtk_tree_path_free (path);
+  priv = GET_PRIV (self);
 
-  g_signal_handlers_unblock_by_func (data->view,
-      individual_view_row_expand_or_collapse_cb,
-      GINT_TO_POINTER (data->expand));
+  if (g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
+      &should_expand))
+    {
+      if (GPOINTER_TO_INT (should_expand))
+        gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
+      else
+        gtk_tree_view_collapse_row (GTK_TREE_VIEW (self), path);
 
-done:
-  g_object_unref (data->view);
-  gtk_tree_row_reference_free (data->row_ref);
-  g_slice_free (ExpandData, data);
+      g_hash_table_remove (priv->expand_groups, name);
+    }
+
+  g_free (name);
+
+  return FALSE;
+}
+
+static gboolean
+individual_view_expand_idle_cb (EmpathyIndividualView *self)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (self);
+
+  DEBUG ("individual_view_expand_idle_cb");
+
+  g_signal_handlers_block_by_func (self,
+    individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+  g_signal_handlers_block_by_func (self,
+    individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
+
+  /* The store/filter could've been removed while we were in the idle queue */
+  if (priv->filter != NULL)
+    {
+      gtk_tree_model_foreach (GTK_TREE_MODEL (priv->filter),
+          (GtkTreeModelForeachFunc) expand_idle_foreach_cb, self);
+    }
+
+  g_signal_handlers_unblock_by_func (self,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
+  g_signal_handlers_unblock_by_func (self,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+
+  /* Empty the table of groups to expand/contract, since it may contain groups
+   * which no longer exist in the tree view. This can happen after going
+   * offline, for example. */
+  g_hash_table_remove_all (priv->expand_groups);
+  priv->expand_groups_idle_handler = 0;
+  g_object_unref (self);
 
   return FALSE;
 }
@@ -1344,9 +1600,9 @@ individual_view_row_has_child_toggled_cb (GtkTreeModel *model,
     EmpathyIndividualView *view)
 {
   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
-  gboolean is_group = FALSE;
+  gboolean should_expand, is_group = FALSE;
   gchar *name = NULL;
-  ExpandData *data;
+  gpointer will_expand;
 
   gtk_tree_model_get (model, iter,
       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
@@ -1359,114 +1615,122 @@ individual_view_row_has_child_toggled_cb (GtkTreeModel *model,
       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 &
+  should_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))
+   * gtk_tree_model_filter_refilter (). We add the rows to expand/contract to
+   * a hash table, and expand or contract them as appropriate all at once in
+   * an idle handler which iterates over all the group rows. */
+  if (!g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
+      &will_expand) ||
+      GPOINTER_TO_INT (will_expand) != should_expand)
     {
-      /* 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);
-}
+      g_hash_table_insert (priv->expand_groups, g_strdup (name),
+          GINT_TO_POINTER (should_expand));
 
-static void
-individual_view_store_row_changed_cb (GtkTreeModel *model,
-  GtkTreePath *path,
-  GtkTreeIter *iter,
-  EmpathyIndividualView *view)
-{
-  individual_view_verify_group_visibility (view, path);
-}
+      if (priv->expand_groups_idle_handler == 0)
+        {
+          priv->expand_groups_idle_handler =
+              g_idle_add ((GSourceFunc) individual_view_expand_idle_cb,
+                  g_object_ref (view));
+        }
+    }
 
-static void
-individual_view_store_row_deleted_cb (GtkTreeModel *model,
-  GtkTreePath *path,
-  EmpathyIndividualView *view)
-{
-  individual_view_verify_group_visibility (view, path);
+  g_free (name);
 }
 
 static gboolean
 individual_view_is_visible_individual (EmpathyIndividualView *self,
-    FolksIndividual *individual)
+    FolksIndividual *individual,
+    gboolean is_online,
+    gboolean is_searching,
+    const gchar *group,
+    gboolean is_fake_group,
+    guint event_count)
 {
   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
   EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
-  const gchar *str;
-  GList *personas, *l;
+  GeeSet *personas;
+  GeeIterator *iter;
+  gboolean is_favorite;
 
-  /* We're only giving the visibility wrt filtering here, not things like
-   * presence. */
-  if (live == NULL || gtk_widget_get_visible (GTK_WIDGET (live)) == FALSE)
+  /* Always display individuals having pending events */
+  if (event_count > 0)
     return TRUE;
 
-  /* check alias name */
-  str = folks_individual_get_alias (individual);
-  if (empathy_live_search_match (live, str))
-    return TRUE;
+  /* We're only giving the visibility wrt filtering here, not things like
+   * presence. */
+  if (!priv->show_untrusted &&
+      folks_individual_get_trust_level (individual) == FOLKS_TRUST_LEVEL_NONE)
+    {
+      return FALSE;
+    }
 
-  /* check contact id, remove the @server.com part */
-  personas = folks_individual_get_personas (individual);
-  for (l = personas; l; l = l->next)
+  if (!priv->show_uninteresting)
     {
-      const gchar *p;
-      gchar *dup_str = NULL;
-      gboolean visible;
+      gboolean contains_interesting_persona = FALSE;
 
-      str = folks_persona_get_uid (l->data);
-      p = strstr (str, "@");
-      if (p != NULL)
-        str = dup_str = g_strndup (str, p - str);
+      /* Hide all individuals which consist entirely of uninteresting
+       * personas */
+      personas = folks_individual_get_personas (individual);
+      iter = gee_iterable_iterator (GEE_ITERABLE (personas));
+      while (!contains_interesting_persona && gee_iterator_next (iter))
+        {
+          FolksPersona *persona = gee_iterator_get (iter);
 
-      visible = empathy_live_search_match (live, str);
-      g_free (dup_str);
-      if (visible)
-        return TRUE;
+          if (empathy_folks_persona_is_interesting (persona))
+            contains_interesting_persona = TRUE;
+
+          g_clear_object (&persona);
+        }
+      g_clear_object (&iter);
+
+      if (!contains_interesting_persona)
+        return FALSE;
     }
 
-  /* FIXME: Add more rules here, we could check phone numbers in
-   * contact's vCard for example. */
+  is_favorite = folks_favourite_details_get_is_favourite (
+      FOLKS_FAVOURITE_DETAILS (individual));
+  if (!is_searching) {
+    if (is_favorite && is_fake_group &&
+        !tp_strdiff (group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
+        /* Always display favorite contacts in the favorite group */
+        return TRUE;
+
+    return (priv->show_offline || is_online);
+  }
 
-  return FALSE;
+  return empathy_individual_match_string (individual,
+      empathy_live_search_get_text (live),
+      empathy_live_search_get_words (live));
+}
+
+static gchar *
+get_group (GtkTreeModel *model,
+    GtkTreeIter *iter,
+    gboolean *is_fake)
+{
+  GtkTreeIter parent_iter;
+  gchar *name = NULL;
+
+  *is_fake = FALSE;
+
+  if (!gtk_tree_model_iter_parent (model, &parent_iter, iter))
+    return NULL;
+
+  gtk_tree_model_get (model, &parent_iter,
+      EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
+      EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, is_fake,
+      -1);
+
+  return name;
 }
 
+
 static gboolean
 individual_view_filter_visible_func (GtkTreeModel *model,
     GtkTreeIter *iter,
@@ -1479,6 +1743,10 @@ individual_view_filter_visible_func (GtkTreeModel *model,
   GtkTreeIter child_iter;
   gboolean visible, is_online;
   gboolean is_searching = TRUE;
+  guint event_count;
+
+  if (priv->custom_filter != NULL)
+    return priv->custom_filter (model, iter, priv->custom_filter_data);
 
   if (priv->search_widget == NULL ||
       !gtk_widget_get_visible (priv->search_widget))
@@ -1489,17 +1757,23 @@ individual_view_filter_visible_func (GtkTreeModel *model,
       EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
       EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
+      EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, &event_count,
       -1);
 
   if (individual != NULL)
     {
-      visible = individual_view_is_visible_individual (self, individual);
+      gchar *group;
+      gboolean is_fake_group;
+
+      group = get_group (model, iter, &is_fake_group);
+
+      visible = individual_view_is_visible_individual (self, individual,
+          is_online, is_searching, group, is_fake_group, event_count);
+
       g_object_unref (individual);
+      g_free (group);
 
-      if (is_searching)
-        return visible;
-      else
-        return (priv->show_offline || is_online);
+      return visible;
     }
 
   if (is_separator)
@@ -1512,20 +1786,28 @@ individual_view_filter_visible_func (GtkTreeModel *model,
   for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
        valid; valid = gtk_tree_model_iter_next (model, &child_iter))
     {
+      gchar *group;
+      gboolean is_fake_group;
+
       gtk_tree_model_get (model, &child_iter,
         EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
         EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
+        EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, &event_count,
         -1);
 
       if (individual == NULL)
         continue;
 
-      visible = individual_view_is_visible_individual (self, individual);
+      group = get_group (model, &child_iter, &is_fake_group);
+
+      visible = individual_view_is_visible_individual (self, individual,
+          is_online, is_searching, group, is_fake_group, event_count);
+
       g_object_unref (individual);
+      g_free (group);
 
       /* show group if it has at least one visible contact in it */
-      if ((is_searching && visible) ||
-          (!is_searching && (priv->show_offline || is_online)))
+      if (visible)
         return TRUE;
     }
 
@@ -1536,28 +1818,10 @@ 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->filter, "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 */
   g_object_set (view,
       "headers-visible", FALSE,
@@ -1612,6 +1876,8 @@ individual_view_constructed (GObject *object)
       "is_group", EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP);
   gtk_tree_view_column_add_attribute (col, cell,
       "compact", EMPATHY_INDIVIDUAL_STORE_COL_COMPACT);
+  gtk_tree_view_column_add_attribute (col, cell,
+      "client-types", EMPATHY_INDIVIDUAL_STORE_COL_CLIENT_TYPES);
 
   /* Audio Call Icon */
   cell = empathy_cell_renderer_activatable_new ();
@@ -1655,12 +1921,6 @@ individual_view_constructed (GObject *object)
     {
       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
@@ -1681,10 +1941,10 @@ individual_view_set_view_features (EmpathyIndividualView *view,
      is enabled).
    */
   gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view),
-      (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DRAG));
+      (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG));
 
   /* Update DnD source/dest */
-  if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DRAG)
+  if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG)
     {
       gtk_drag_source_set (GTK_WIDGET (view),
           GDK_BUTTON1_MASK,
@@ -1698,7 +1958,7 @@ individual_view_set_view_features (EmpathyIndividualView *view,
 
     }
 
-  if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DROP)
+  if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP)
     {
       gtk_drag_dest_set (GTK_WIDGET (view),
           GTK_DEST_DEFAULT_ALL,
@@ -1713,7 +1973,7 @@ individual_view_set_view_features (EmpathyIndividualView *view,
 
   /* Update has-tooltip */
   has_tooltip =
-      (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_TOOLTIP) != 0;
+      (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP) != 0;
   gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
 }
 
@@ -1725,14 +1985,25 @@ individual_view_dispose (GObject *object)
 
   tp_clear_object (&priv->store);
   tp_clear_object (&priv->filter);
-  tp_clear_pointer (&priv->tooltip_widget, gtk_widget_destroy);
-  tp_clear_pointer (&priv->file_targets, gtk_target_list_unref);
+  tp_clear_object (&priv->tooltip_widget);
 
   empathy_individual_view_set_live_search (view, NULL);
 
   G_OBJECT_CLASS (empathy_individual_view_parent_class)->dispose (object);
 }
 
+static void
+individual_view_finalize (GObject *object)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (object);
+
+  if (priv->expand_groups_idle_handler != 0)
+    g_source_remove (priv->expand_groups_idle_handler);
+  g_hash_table_unref (priv->expand_groups);
+
+  G_OBJECT_CLASS (empathy_individual_view_parent_class)->finalize (object);
+}
+
 static void
 individual_view_get_property (GObject *object,
     guint param_id,
@@ -1757,6 +2028,12 @@ individual_view_get_property (GObject *object,
     case PROP_SHOW_OFFLINE:
       g_value_set_boolean (value, priv->show_offline);
       break;
+    case PROP_SHOW_UNTRUSTED:
+      g_value_set_boolean (value, priv->show_untrusted);
+      break;
+    case PROP_SHOW_UNINTERESTING:
+      g_value_set_boolean (value, priv->show_uninteresting);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
       break;
@@ -1775,7 +2052,7 @@ individual_view_set_property (GObject *object,
   switch (param_id)
     {
     case PROP_STORE:
-      priv->store = g_value_dup_object (value);
+      empathy_individual_view_set_store (view, g_value_get_object (value));
       break;
     case PROP_VIEW_FEATURES:
       individual_view_set_view_features (view, g_value_get_flags (value));
@@ -1787,6 +2064,13 @@ individual_view_set_property (GObject *object,
       empathy_individual_view_set_show_offline (view,
           g_value_get_boolean (value));
       break;
+    case PROP_SHOW_UNTRUSTED:
+      empathy_individual_view_set_show_untrusted (view,
+          g_value_get_boolean (value));
+      break;
+    case PROP_SHOW_UNINTERESTING:
+      empathy_individual_view_set_show_uninteresting (view,
+          g_value_get_boolean (value));
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
       break;
@@ -1802,6 +2086,7 @@ empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
 
   object_class->constructed = individual_view_constructed;
   object_class->dispose = individual_view_dispose;
+  object_class->finalize = individual_view_finalize;
   object_class->get_property = individual_view_get_property;
   object_class->set_property = individual_view_set_property;
 
@@ -1817,14 +2102,26 @@ empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
    * won't be called. */
   tree_view_class->row_activated = individual_view_row_activated;
 
-  signals[DRAG_CONTACT_RECEIVED] =
-      g_signal_new ("drag-contact-received",
+  klass->drag_individual_received = real_drag_individual_received_cb;
+
+  signals[DRAG_INDIVIDUAL_RECEIVED] =
+      g_signal_new ("drag-individual-received",
       G_OBJECT_CLASS_TYPE (klass),
       G_SIGNAL_RUN_LAST,
-      0,
+      G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_individual_received),
       NULL, NULL,
-      _empathy_gtk_marshal_VOID__OBJECT_STRING_STRING,
-      G_TYPE_NONE, 3, EMPATHY_TYPE_CONTACT, G_TYPE_STRING, G_TYPE_STRING);
+      g_cclosure_marshal_generic,
+      G_TYPE_NONE, 4, G_TYPE_UINT, FOLKS_TYPE_INDIVIDUAL,
+      G_TYPE_STRING, G_TYPE_STRING);
+
+  signals[DRAG_PERSONA_RECEIVED] =
+      g_signal_new ("drag-persona-received",
+      G_OBJECT_CLASS_TYPE (klass),
+      G_SIGNAL_RUN_LAST,
+      G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_persona_received),
+      NULL, NULL,
+      g_cclosure_marshal_generic,
+      G_TYPE_BOOLEAN, 3, G_TYPE_UINT, FOLKS_TYPE_PERSONA, FOLKS_TYPE_INDIVIDUAL);
 
   g_object_class_install_property (object_class,
       PROP_STORE,
@@ -1832,7 +2129,7 @@ empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
           "The store of the view",
           "The store of the view",
           EMPATHY_TYPE_INDIVIDUAL_STORE,
-          G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
+          G_PARAM_READWRITE));
   g_object_class_install_property (object_class,
       PROP_VIEW_FEATURES,
       g_param_spec_flags ("view-features",
@@ -1843,16 +2140,30 @@ empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
   g_object_class_install_property (object_class,
       PROP_INDIVIDUAL_FEATURES,
       g_param_spec_flags ("individual-features",
-          "Features of the contact menu",
+          "Features of the individual menu",
           "Flags for all enabled features for the menu",
           EMPATHY_TYPE_INDIVIDUAL_FEATURE_FLAGS,
-          EMPATHY_CONTACT_FEATURE_NONE, G_PARAM_READWRITE));
+          EMPATHY_INDIVIDUAL_FEATURE_NONE, 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_UNTRUSTED,
+      g_param_spec_boolean ("show-untrusted",
+          "Show Untrusted Individuals",
+          "Whether the view should display untrusted individuals; "
+          "those who could not be who they say they are.",
+          TRUE, G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_SHOW_UNINTERESTING,
+      g_param_spec_boolean ("show-uninteresting",
+          "Show Uninteresting Individuals",
+          "Whether the view should not filter out individuals using "
+          "empathy_folks_persona_is_interesting.",
+          FALSE, G_PARAM_READWRITE));
 
   g_type_class_add_private (object_class, sizeof (EmpathyIndividualViewPriv));
 }
@@ -1864,16 +2175,19 @@ empathy_individual_view_init (EmpathyIndividualView *view)
       EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewPriv);
 
   view->priv = priv;
+
+  priv->show_untrusted = TRUE;
+  priv->show_uninteresting = FALSE;
+
   /* Get saved group states. */
   empathy_contact_groups_get_all ();
 
+  priv->expand_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
+      (GDestroyNotify) g_free, NULL);
+
   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);
@@ -1905,7 +2219,6 @@ empathy_individual_view_new (EmpathyIndividualStore *store,
 FolksIndividual *
 empathy_individual_view_dup_selected (EmpathyIndividualView *view)
 {
-  EmpathyIndividualViewPriv *priv;
   GtkTreeSelection *selection;
   GtkTreeIter iter;
   GtkTreeModel *model;
@@ -1913,8 +2226,6 @@ empathy_individual_view_dup_selected (EmpathyIndividualView *view)
 
   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;
@@ -1925,34 +2236,10 @@ empathy_individual_view_dup_selected (EmpathyIndividualView *view)
   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,
+static gchar *
+empathy_individual_view_dup_selected_group (EmpathyIndividualView *view,
     gboolean *is_fake_group)
 {
-  EmpathyIndividualViewPriv *priv;
   GtkTreeSelection *selection;
   GtkTreeIter iter;
   GtkTreeModel *model;
@@ -1962,8 +2249,6 @@ empathy_individual_view_get_selected_group (EmpathyIndividualView *view,
 
   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;
@@ -1985,19 +2270,51 @@ empathy_individual_view_get_selected_group (EmpathyIndividualView *view,
   return name;
 }
 
-static gboolean
+enum
+{
+  REMOVE_DIALOG_RESPONSE_CANCEL = 0,
+  REMOVE_DIALOG_RESPONSE_DELETE,
+  REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK,
+};
+
+static int
 individual_view_remove_dialog_show (GtkWindow *parent,
     const gchar *message,
-    const gchar *secondary_text)
+    const gchar *secondary_text,
+    gboolean block_button,
+    GdkPixbuf *avatar)
 {
   GtkWidget *dialog;
   gboolean res;
 
   dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
       GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message);
+
+  if (avatar != NULL)
+    {
+      GtkWidget *image = gtk_image_new_from_pixbuf (avatar);
+      gtk_message_dialog_set_image (GTK_MESSAGE_DIALOG (dialog), image);
+      gtk_widget_show (image);
+    }
+
+  if (block_button)
+    {
+      GtkWidget *button;
+
+      /* gtk_dialog_add_button() doesn't allow us to pass a string with a
+       * mnemonic so we have to create the button manually. */
+      button = gtk_button_new_with_mnemonic (
+          _("Delete and _Block"));
+
+      gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button,
+          REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK);
+
+      gtk_widget_show (button);
+    }
+
   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
-      GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
-      GTK_STOCK_DELETE, GTK_RESPONSE_YES, NULL);
+      GTK_STOCK_CANCEL, REMOVE_DIALOG_RESPONSE_CANCEL,
+      GTK_STOCK_DELETE, REMOVE_DIALOG_RESPONSE_DELETE, NULL);
   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
       "%s", secondary_text);
 
@@ -2006,7 +2323,7 @@ individual_view_remove_dialog_show (GtkWindow *parent,
   res = gtk_dialog_run (GTK_DIALOG (dialog));
   gtk_widget_destroy (dialog);
 
-  return (res == GTK_RESPONSE_YES);
+  return res;
 }
 
 static void
@@ -2015,7 +2332,7 @@ individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
 {
   gchar *group;
 
-  group = empathy_individual_view_get_selected_group (view, NULL);
+  group = empathy_individual_view_dup_selected_group (view, NULL);
   if (group != NULL)
     {
       gchar *text;
@@ -2026,7 +2343,7 @@ individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
           group);
       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
       if (individual_view_remove_dialog_show (parent, _("Removing group"),
-              text))
+              text, FALSE, NULL) == REMOVE_DIALOG_RESPONSE_DELETE)
         {
           EmpathyIndividualManager *manager =
               empathy_individual_manager_dup_singleton ();
@@ -2056,10 +2373,11 @@ empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
               EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)))
     return NULL;
 
-  group = empathy_individual_view_get_selected_group (view, &is_fake_group);
+  group = empathy_individual_view_dup_selected_group (view, &is_fake_group);
   if (!group || is_fake_group)
     {
       /* We can't alter fake groups */
+      g_free (group);
       return NULL;
     }
 
@@ -2095,38 +2413,143 @@ empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
 }
 
 static void
-individual_view_remove_activate_cb (GtkMenuItem *menuitem,
-    EmpathyIndividualView *view)
+got_avatar (GObject *source_object,
+    GAsyncResult *result,
+    gpointer user_data)
 {
-  FolksIndividual *individual;
+  FolksIndividual *individual = FOLKS_INDIVIDUAL (source_object);
+  EmpathyIndividualView *view = user_data;
+  EmpathyIndividualViewPriv *priv = GET_PRIV (view);
+  GdkPixbuf *avatar;
+  EmpathyIndividualManager *manager;
+  gchar *text;
+  GtkWindow *parent;
+  GeeSet *personas;
+  guint persona_count = 0;
+  gboolean can_block;
+  GError *error = NULL;
+  gint res;
 
-  individual = empathy_individual_view_dup_selected (view);
+  avatar = empathy_pixbuf_avatar_from_individual_scaled_finish (individual,
+      result, &error);
 
-  if (individual != NULL)
+  if (error != NULL)
     {
-      gchar *text;
-      GtkWindow *parent;
+      DEBUG ("Could not get avatar: %s", error->message);
+      g_error_free (error);
+    }
 
-      parent = empathy_get_toplevel_window (GTK_WIDGET (view));
+  /* We couldn't retrieve the avatar, but that isn't a fatal error,
+   * so we still display the remove dialog. */
+
+  personas = folks_individual_get_personas (individual);
+
+  if (priv->show_uninteresting)
+    {
+      persona_count = gee_collection_get_size (GEE_COLLECTION (personas));
+    }
+  else
+    {
+      GeeIterator *iter;
+
+      iter = gee_iterable_iterator (GEE_ITERABLE (personas));
+      while (persona_count < 2 && gee_iterator_next (iter))
+        {
+          FolksPersona *persona = gee_iterator_get (iter);
+
+          if (empathy_folks_persona_is_interesting (persona))
+            persona_count++;
+
+          g_clear_object (&persona);
+        }
+      g_clear_object (&iter);
+    }
+
+  /* If we have more than one TpfPersona, display a different message
+   * ensuring the user knows that *all* of the meta-contacts' personas will
+   * be removed. */
+
+  if (persona_count < 2)
+    {
+      /* Not a meta-contact */
+      text =
+          g_strdup_printf (
+              _("Do you really want to remove the contact '%s'?"),
+              folks_alias_details_get_alias (
+                  FOLKS_ALIAS_DETAILS (individual)));
+    }
+  else
+    {
+      /* Meta-contact */
       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))
+          g_strdup_printf (
+              _("Do you really want to remove the linked contact '%s'? "
+                "Note that this will remove all the contacts which make up "
+                "this linked contact."),
+              folks_alias_details_get_alias (
+                  FOLKS_ALIAS_DETAILS (individual)));
+    }
+
+
+  manager = empathy_individual_manager_dup_singleton ();
+  can_block = empathy_individual_manager_supports_blocking (manager,
+      individual);
+  parent = empathy_get_toplevel_window (GTK_WIDGET (view));
+  res = individual_view_remove_dialog_show (parent, _("Removing contact"),
+          text, can_block, avatar);
+
+  if (res == REMOVE_DIALOG_RESPONSE_DELETE ||
+      res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
+    {
+      gboolean abusive;
+
+      if (res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
         {
-          EmpathyIndividualManager *manager;
+          if (!empathy_block_individual_dialog_show (parent, individual,
+                avatar, &abusive))
+            goto finally;
 
-          manager = empathy_individual_manager_dup_singleton ();
-          empathy_individual_manager_remove (manager, individual, "");
-          g_object_unref (G_OBJECT (manager));
+          empathy_individual_manager_set_blocked (manager, individual,
+              TRUE, abusive);
         }
 
-      g_free (text);
+      empathy_individual_manager_remove (manager, individual, "");
+    }
+
+ finally:
+  g_free (text);
+  g_object_unref (manager);
+}
+
+static void
+individual_view_remove_activate_cb (GtkMenuItem *menuitem,
+    EmpathyIndividualView *view)
+{
+  FolksIndividual *individual;
+
+  individual = empathy_individual_view_dup_selected (view);
+
+  if (individual != NULL)
+    {
+      empathy_pixbuf_avatar_from_individual_scaled_async (individual,
+          48, 48, NULL, got_avatar, view);
       g_object_unref (individual);
     }
 }
 
+static void
+individual_menu_link_contacts_activated_cb (EmpathyIndividualMenu *menu,
+    EmpathyLinkingDialog *linking_dialog,
+    EmpathyIndividualView *self)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (self);
+  EmpathyIndividualLinker *linker;
+
+  linker = empathy_linking_dialog_get_individual_linker (linking_dialog);
+  empathy_individual_linker_set_search_text (linker,
+      empathy_live_search_get_text (EMPATHY_LIVE_SEARCH (priv->search_widget)));
+}
+
 GtkWidget *
 empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
 {
@@ -2135,24 +2558,51 @@ empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
   GtkWidget *menu = NULL;
   GtkWidget *item;
   GtkWidget *image;
-  EmpathyIndividualManagerFlags flags;
+  gboolean can_remove = FALSE;
+  GeeSet *personas;
+  GeeIterator *iter;
 
   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
 
+  if (priv->individual_features == EMPATHY_INDIVIDUAL_FEATURE_NONE)
+    /* No need to create a context menu */
+    return NULL;
+
   individual = empathy_individual_view_dup_selected (view);
   if (individual == NULL)
     return NULL;
 
-  flags = empathy_individual_view_get_flags (view);
+  if (!empathy_folks_individual_contains_contact (individual))
+    goto out;
+
+  /* If any of the Individual's personas can be removed, add an option to
+   * remove. This will act as a best-effort option. If any Personas cannot be
+   * removed from the server, then this option will just be inactive upon
+   * subsequent menu openings */
+  personas = folks_individual_get_personas (individual);
+  iter = gee_iterable_iterator (GEE_ITERABLE (personas));
+  while (!can_remove && gee_iterator_next (iter))
+    {
+      FolksPersona *persona = gee_iterator_get (iter);
+      FolksPersonaStore *store = folks_persona_get_store (persona);
+      FolksMaybeBool maybe_can_remove =
+          folks_persona_store_get_can_remove_personas (store);
+
+      if (maybe_can_remove == FOLKS_MAYBE_BOOL_TRUE)
+        can_remove = TRUE;
+
+      g_clear_object (&persona);
+    }
+  g_clear_object (&iter);
 
-  menu = empathy_individual_menu_new (individual, priv->individual_features);
+  menu = empathy_individual_menu_new (individual, priv->individual_features,
+      priv->store);
 
   /* Remove contact */
-  if (priv->view_features &
-      EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_REMOVE &&
-      flags & EMPATHY_INDIVIDUAL_MANAGER_CAN_REMOVE)
+  if ((priv->view_features &
+      EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_REMOVE) &&
+      can_remove)
     {
-
       /* create the menu if required, or just add a separator */
       if (menu == NULL)
         menu = gtk_menu_new ();
@@ -2174,6 +2624,13 @@ empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
           G_CALLBACK (individual_view_remove_activate_cb), view);
     }
 
+  /* Connect to EmpathyIndividualMenu::link-contacts-activated so that we can
+   * set the live search text on the new linking dialogue to be the same as
+   * our own. */
+  g_signal_connect (menu, "link-contacts-activated",
+      (GCallback) individual_menu_link_contacts_activated_cb, view);
+
+out:
   g_object_unref (individual);
 
   return menu;
@@ -2266,3 +2723,144 @@ empathy_individual_view_set_show_offline (EmpathyIndividualView *self,
   g_object_notify (G_OBJECT (self), "show-offline");
   gtk_tree_model_filter_refilter (priv->filter);
 }
+
+gboolean
+empathy_individual_view_get_show_untrusted (EmpathyIndividualView *self)
+{
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
+
+  return GET_PRIV (self)->show_untrusted;
+}
+
+void
+empathy_individual_view_set_show_untrusted (EmpathyIndividualView *self,
+    gboolean show_untrusted)
+{
+  EmpathyIndividualViewPriv *priv;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
+
+  priv = GET_PRIV (self);
+
+  priv->show_untrusted = show_untrusted;
+
+  g_object_notify (G_OBJECT (self), "show-untrusted");
+  gtk_tree_model_filter_refilter (priv->filter);
+}
+
+EmpathyIndividualStore *
+empathy_individual_view_get_store (EmpathyIndividualView *self)
+{
+  g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), NULL);
+
+  return GET_PRIV (self)->store;
+}
+
+void
+empathy_individual_view_set_store (EmpathyIndividualView *self,
+    EmpathyIndividualStore *store)
+{
+  EmpathyIndividualViewPriv *priv;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
+  g_return_if_fail (store == NULL || EMPATHY_IS_INDIVIDUAL_STORE (store));
+
+  priv = GET_PRIV (self);
+
+  /* Destroy the old filter and remove the old store */
+  if (priv->store != NULL)
+    {
+      g_signal_handlers_disconnect_by_func (priv->filter,
+          individual_view_row_has_child_toggled_cb, self);
+
+      gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
+    }
+
+  tp_clear_object (&priv->filter);
+  tp_clear_object (&priv->store);
+
+  /* Set the new store */
+  priv->store = store;
+
+  if (store != NULL)
+    {
+      g_object_ref (store);
+
+      /* Create a new filter */
+      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, self, NULL);
+
+      g_signal_connect (priv->filter, "row-has-child-toggled",
+          G_CALLBACK (individual_view_row_has_child_toggled_cb), self);
+      gtk_tree_view_set_model (GTK_TREE_VIEW (self),
+          GTK_TREE_MODEL (priv->filter));
+    }
+}
+
+void
+empathy_individual_view_start_search (EmpathyIndividualView *self)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (self);
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
+  g_return_if_fail (priv->search_widget != NULL);
+
+  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));
+}
+
+void
+empathy_individual_view_set_custom_filter (EmpathyIndividualView *self,
+    GtkTreeModelFilterVisibleFunc filter,
+    gpointer data)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (self);
+
+  priv->custom_filter = filter;
+  priv->custom_filter_data = data;
+}
+
+void
+empathy_individual_view_refilter (EmpathyIndividualView *self)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (self);
+
+  gtk_tree_model_filter_refilter (priv->filter);
+}
+
+void
+empathy_individual_view_select_first (EmpathyIndividualView *self)
+{
+  EmpathyIndividualViewPriv *priv = GET_PRIV (self);
+  GtkTreeIter iter;
+
+  gtk_tree_model_filter_refilter (priv->filter);
+
+  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->filter), &iter))
+    {
+      GtkTreeSelection *selection = gtk_tree_view_get_selection (
+          GTK_TREE_VIEW (self));
+
+      gtk_tree_selection_select_iter (selection, &iter);
+    }
+}
+
+void
+empathy_individual_view_set_show_uninteresting (EmpathyIndividualView *self,
+    gboolean show_uninteresting)
+{
+  EmpathyIndividualViewPriv *priv;
+
+  g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
+
+  priv = GET_PRIV (self);
+
+  priv->show_uninteresting = show_uninteresting;
+
+  g_object_notify (G_OBJECT (self), "show-uninteresting");
+  gtk_tree_model_filter_refilter (priv->filter);
+}