Merge remote branch 'pwith/favourites'
authorTravis Reitter <travis.reitter@collabora.co.uk>
Tue, 29 Jun 2010 23:50:34 +0000 (16:50 -0700)
committerTravis Reitter <travis.reitter@collabora.co.uk>
Tue, 29 Jun 2010 23:50:34 +0000 (16:50 -0700)
(Fixed)
Conflicts:
folks/individual.vala
folks/persona.vala

.gitignore
backends/telepathy/Makefile.am
backends/telepathy/tpf-logger.vala [new file with mode: 0644]
backends/telepathy/tpf-persona-store.vala
backends/telepathy/tpf-persona.vala
configure.ac
folks/Makefile.am
folks/favourite.vala [new file with mode: 0644]
folks/individual.vala
folks/persona.vala

index f6d3361..571a659 100644 (file)
@@ -60,6 +60,7 @@ Makefile.in
 /backends/telepathy/tp-backend-factory.c
 /backends/telepathy/tpf-persona.c
 /backends/telepathy/tpf-persona-store.c
+/backends/telepathy/tpf-logger.c
 /folks/alias.c
 /folks/avatar.c
 /folks/backend.c
@@ -70,6 +71,7 @@ Makefile.in
 /folks/persona.c
 /folks/persona-store.c
 /folks/presence.c
+/folks/favourite.c
 /folks/folks.h
 /folks/folks.pc
 /folks/folks-uninstalled.pc
index af3963f..a06b200 100644 (file)
@@ -75,6 +75,7 @@ pkgconfig_DATA = $(pkgconfig_in:.in=)
 libfolks_telepathy_la_VALASOURCES = \
        tpf-persona.vala \
        tpf-persona-store.vala \
+       tpf-logger.vala \
        $(NULL)
 
 libfolks_telepathy_la_SOURCES = \
diff --git a/backends/telepathy/tpf-logger.vala b/backends/telepathy/tpf-logger.vala
new file mode 100644 (file)
index 0000000..1119379
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip.withnall@collabora.co.uk>
+ */
+
+using GLib;
+using Gee;
+using Tp;
+using Folks;
+
+private struct AccountFavourites
+{
+  DBus.ObjectPath account_path;
+  string[] ids;
+}
+
+[DBus (name = "org.freedesktop.Telepathy.Logger.DRAFT")]
+private interface LoggerIface : DBus.Object
+{
+  public abstract async AccountFavourites[] get_favourite_contacts ()
+      throws DBus.Error;
+  public abstract async void add_favourite_contact (
+      DBus.ObjectPath account_path, string id) throws DBus.Error;
+  public abstract async void remove_favourite_contact (
+      DBus.ObjectPath account_path, string id) throws DBus.Error;
+
+  public abstract signal void favourite_contacts_changed (
+      DBus.ObjectPath account_path, string[] added, string[] removed);
+}
+
+internal class Logger : GLib.Object
+{
+  private static LoggerIface logger;
+  private string account_path;
+
+  public signal void favourite_contacts_changed (string[] added,
+      string[] removed);
+
+  public Logger (string account_path) throws DBus.Error
+    {
+      if (this.logger == null)
+        {
+          /* Create a logger proxy for favourites support */
+          /* FIXME: This should be ported to the Vala GDBus stuff and made
+           * async, but that depends on
+           * https://bugzilla.gnome.org/show_bug.cgi?id=622611 being fixed.
+           * FIXME: If this is made async, race conditions may appear in
+           * TpfPersonaStore, which will need to be prevented. e.g.
+           * change_is_favourite() may get called before logger initialisation
+           * is complete; favourites-change requests should be queued. */
+          var dbus_conn = DBus.Bus.get (DBus.BusType.SESSION);
+          this.logger = dbus_conn.get_object (
+              "org.freedesktop.Telepathy.Logger",
+              "/org/freedesktop/Telepathy/Logger",
+              "org.freedesktop.Telepathy.Logger.DRAFT") as LoggerIface;
+        }
+
+      this.account_path = account_path;
+      this.logger.favourite_contacts_changed.connect ((ap, a, r) =>
+        {
+          if (ap != this.account_path)
+            return;
+
+          this.favourite_contacts_changed (a, r);
+        });
+    }
+
+  public async string[] get_favourite_contacts () throws DBus.Error
+    {
+      AccountFavourites[] favs = yield this.logger.get_favourite_contacts ();
+
+      foreach (AccountFavourites account in favs)
+        {
+          /* We only want the favourites from this account */
+          if (account.account_path == this.account_path)
+            return account.ids;
+        }
+
+      return {};
+    }
+
+  public async void add_favourite_contact (string id) throws DBus.Error
+    {
+      yield this.logger.add_favourite_contact (
+          new DBus.ObjectPath (this.account_path), id);
+    }
+
+  public async void remove_favourite_contact (string id) throws DBus.Error
+    {
+      yield this.logger.remove_favourite_contact (
+          new DBus.ObjectPath (this.account_path), id);
+    }
+}
index 8c7ca2c..f179941 100644 (file)
@@ -16,6 +16,7 @@
  *
  * Authors:
  *       Travis Reitter <travis.reitter@collabora.co.uk>
+ *       Philip Withnall <philip.withnall@collabora.co.uk>
  */
 
 using GLib;
@@ -38,12 +39,15 @@ public class Tpf.PersonaStore : Folks.PersonaStore
   private HashMap<string, Channel> standard_channels_unready;
   private HashMap<string, Channel> group_channels_unready;
   private HashMap<string, Channel> groups;
+  /* FIXME: Should be HashSet<Handle> */
+  private HashSet<uint> favourite_handles;
   private Channel publish;
   private Channel stored;
   private Channel subscribe;
   private Connection conn;
   private TpLowlevel ll;
   private AccountManager account_manager;
+  private Logger logger;
 
   [Property(nick = "basis account",
       blurb = "Telepathy account this store is based upon")]
@@ -78,6 +82,7 @@ public class Tpf.PersonaStore : Folks.PersonaStore
       this.standard_channels_unready = new HashMap<string, Channel> ();
       this.group_channels_unready = new HashMap<string, Channel> ();
       this.groups = new HashMap<string, Channel> ();
+      this.favourite_handles = new HashSet<uint> ();
       this.ll = new TpLowlevel ();
       this.account_manager = AccountManager.dup ();
 
@@ -108,6 +113,134 @@ public class Tpf.PersonaStore : Folks.PersonaStore
           this.account_status_changed_cb (Tp.ConnectionStatus.DISCONNECTED,
               status, reason, null, null);
         }
+
+      try
+        {
+          this.logger = new Logger (this.id);
+          this.logger.favourite_contacts_changed.connect (
+              this.favourite_contacts_changed_cb);
+        }
+      catch (DBus.Error e)
+        {
+          warning ("couldn't connect to the telepathy-logger service");
+          this.logger = null;
+        }
+    }
+
+  private async void initialise_favourite_contacts ()
+    {
+      /* Get an initial set of favourite contacts */
+      try
+        {
+          string[] contacts = yield this.logger.get_favourite_contacts ();
+
+          if (contacts.length == 0)
+            return;
+
+          /* Note that we don't need to release these handles, as they're
+           * also held by the relevant contact objects, and will be released
+           * as appropriate by those objects (we're circumventing tp-glib's
+           * handle reference counting). */
+          this.conn.request_handles (-1, HandleType.CONTACT, contacts,
+            (c, ht, nh, h, i, e, w) =>
+              {
+                try
+                  {
+                    this.change_favourites_by_request_handles (nh, h, i, e,
+                        true);
+                  }
+                catch (GLib.Error e)
+                  {
+                    warning ("couldn't get list of favourite contacts: %s",
+                        e.message);
+                  }
+              },
+            this);
+          /* FIXME: Have to pass this as weak_object parameter since Vala
+           * seems to swap the order of user_data and weak_object in the
+           * callback. */
+        }
+      catch (DBus.Error e)
+        {
+          warning ("couldn't get list of favourite contacts: %s", e.message);
+        }
+    }
+
+  private void change_favourites_by_request_handles (uint n_handles,
+      Handle[] handles, string[] ids, GLib.Error? error,
+      bool add) throws GLib.Error
+    {
+      if (error != null)
+        throw error;
+
+      for (var i = 0; i < n_handles; i++)
+        {
+          Handle h = handles[i];
+          Persona p = this.handle_persona_map[h];
+
+          /* Add/Remove the handle to the set of favourite handles, since we
+           * might not have the corresponding contact yet */
+          if (add)
+            this.favourite_handles.add (h);
+          else
+            this.favourite_handles.remove (h);
+
+          if (p == null)
+            {
+              warning ("unknown persona '%s' in favourites list", ids[i]);
+              continue;
+            }
+
+          /* Mark or unmark the persona as a favourite */
+          p.is_favourite = add;
+        }
+    }
+
+  private void favourite_contacts_changed_cb (string[] added, string[] removed)
+    {
+      /* Don't listen to favourites updates if the account is disconnected. */
+      if (this.conn == null)
+        return;
+
+      /* Add favourites */
+      if (added.length > 0)
+        {
+          this.conn.request_handles (-1, HandleType.CONTACT, added,
+              (c, ht, nh, h, i, e, w) =>
+                {
+                  try
+                    {
+                      this.change_favourites_by_request_handles (nh, h, i, e,
+                          true);
+                    }
+                  catch (GLib.Error e)
+                    {
+                      warning ("couldn't add favourite contacts: %s",
+                          e.message);
+                    }
+                },
+              this);
+        }
+
+      /* Remove favourites */
+      if (removed.length > 0)
+        {
+          this.conn.request_handles (-1, HandleType.CONTACT, removed,
+              (c, ht, nh, h, i, e, w) =>
+                {
+                  try
+                    {
+                      this.change_favourites_by_request_handles (nh, h, i, e,
+                          false);
+                    }
+                  catch (GLib.Error e)
+                    {
+                      warning ("couldn't remove favourite contacts: %s",
+                          e.message);
+                    }
+                },
+              this);
+        }
     }
 
   private void account_status_changed_cb (ConnectionStatus old_status,
@@ -130,6 +263,9 @@ public class Tpf.PersonaStore : Folks.PersonaStore
       this.add_standard_channel (conn, "stored");
       this.add_standard_channel (conn, "subscribe");
       this.conn = conn;
+
+      /* We can only initialise the favourite contacts once we've got conn */
+      this.initialise_favourite_contacts.begin ();
     }
 
   private void new_group_channels_cb (void *data)
@@ -697,8 +833,15 @@ public class Tpf.PersonaStore : Folks.PersonaStore
                 {
                   personas_new.insert (persona.iid, persona);
 
+                  var h = contact.get_handle ();
                   this._personas.insert (persona.iid, persona);
-                  this.handle_persona_map[contact.get_handle ()] = persona;
+                  this.handle_persona_map[h] = persona;
+
+                  /* If the handle is a favourite, ensure the persona's marked
+                   * as such. This deals with the case where we receive a
+                   * contact _after_ we've discovered that they're a
+                   * favourite. */
+                  persona.is_favourite = this.favourite_handles.contains (h);
                 }
             }
           catch (Tp.Error e)
@@ -825,4 +968,33 @@ public class Tpf.PersonaStore : Folks.PersonaStore
 
       return null;
     }
+
+  public async void change_is_favourite (Folks.Persona persona,
+      bool is_favourite)
+    {
+      /* It's possible for us to not be able to connect to the logger;
+       * see connection_ready_cb() */
+      if (this.logger == null)
+        {
+          warning ("failed to change favourite without connection to the " +
+                   "telepathy-logger service");
+          return;
+        }
+
+      try
+        {
+          /* Add or remove the persona to the list of favourites as
+           * appropriate. */
+          var id = ((Tpf.Persona) persona).contact.get_identifier ();
+
+          if (is_favourite)
+            yield this.logger.add_favourite_contact (id);
+          else
+            yield this.logger.remove_favourite_contact (id);
+        }
+      catch (DBus.Error e)
+        {
+          warning ("failed to change a persona's favourite status");
+        }
+    }
 }
index 9f82e52..fdac879 100644 (file)
@@ -24,9 +24,10 @@ using Tp;
 using Folks;
 
 public class Tpf.Persona : Folks.Persona, Alias, Avatar, Folks.Capabilities,
-       Groups, Presence
+       Groups, Presence, Favourite
 {
   private HashTable<string, bool> _groups;
+  private bool _is_favourite;
 
   /* interface Alias */
   public override string alias { get; set; }
@@ -41,6 +42,21 @@ public class Tpf.Persona : Folks.Persona, Alias, Avatar, Folks.Capabilities,
   public override Folks.PresenceType presence_type { get; private set; }
   public override string presence_message { get; private set; }
 
+  /* interface Favourite */
+  public override bool is_favourite
+    {
+      get { return this._is_favourite; }
+
+      set
+        {
+          if (this._is_favourite == value)
+            return;
+
+          ((Tpf.PersonaStore) this.store).change_is_favourite (this, value);
+          this._is_favourite = value;
+        }
+    }
+
   /* interface Groups */
   public HashTable<string, bool> groups
     {
index ad8b1ca..6168f47 100644 (file)
@@ -3,7 +3,7 @@ m4_define(folks_released, 0)
 
 m4_define([folks_major_version], [0])
 m4_define([folks_minor_version], [1])
-m4_define([folks_micro_version], [5])
+m4_define([folks_micro_version], [6])
 m4_define([folks_nano_version], [0])
 
 dnl Display the nano_version only if it's not '0'
index 54b6782..7d553e9 100644 (file)
@@ -29,6 +29,7 @@ libfolks_la_VALASOURCES = \
        backend.vala \
        backend-store.vala \
        capabilities.vala \
+       favourite.vala \
        groups.vala \
        individual.vala \
        individual-aggregator.vala \
diff --git a/folks/favourite.vala b/folks/favourite.vala
new file mode 100644 (file)
index 0000000..cc47b01
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip.withnall@collabora.co.uk>
+ */
+
+using GLib;
+
+/**
+ * Interface exposing a {@link Persona}'s or {@link Individual}'s user-defined
+ * status as a favourite.
+ */
+public interface Folks.Favourite : Object
+{
+  /**
+   * Whether this contact is a user-defined favourite.
+   */
+  public abstract bool is_favourite { get; set; }
+}
index 56d2721..13a4e3f 100644 (file)
@@ -27,11 +27,12 @@ using Folks;
  * might have, such as their different IM addresses or vCard entries.
  */
 public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
-       Presence
+       Presence, Favourite
 {
   private HashTable<string, bool> _groups;
   private GLib.List<Persona> _personas;
   private HashTable<PersonaStore, HashSet<Persona>> stores;
+  private bool _is_favourite;
 
   /* XXX: should setting this push it down into the Persona (to foward along to
    * the actual store if possible?) */
@@ -80,6 +81,35 @@ public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
   public signal void removed ();
 
   /**
+   * Whether this Individual is a user-defined favourite.
+   *
+   * This property is `true` if any of this Individual's {@link Persona}s are
+   * favourites).
+   *
+   * When set, the value is propagated to all of this Individual's
+   * {@link Persona}s.
+   */
+  public bool is_favourite
+    {
+      get { return this._is_favourite; }
+
+      /* Propagate the new favourite status to every Persona, but only if it's
+       * changed. */
+      set
+        {
+          if (this._is_favourite == value)
+            return;
+
+          this._is_favourite = value;
+          this._personas.foreach ((p) =>
+            {
+              if (p is Favourite)
+                ((Favourite) p).is_favourite = value;
+            });
+        }
+    }
+
+  /**
    * {@inheritDoc}
    */
   public HashTable<string, bool> groups
@@ -121,6 +151,8 @@ public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
                   this.notify_presence_cb);
               persona.notify["presence-type"].disconnect (
                   this.notify_presence_cb);
+              persona.notify["is-favourite"].disconnect (
+                  this.notify_is_favourite_cb);
               groups.group_changed.disconnect (this.persona_group_changed_cb);
             });
 
@@ -148,6 +180,8 @@ public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
               persona.notify["presence-message"].connect (
                   this.notify_presence_cb);
               persona.notify["presence-type"].connect (this.notify_presence_cb);
+              persona.notify["is-favourite"].connect (
+                  this.notify_is_favourite_cb);
               groups.group_changed.connect (this.persona_group_changed_cb);
             });
 
@@ -196,6 +230,11 @@ public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
       this.update_presence ();
     }
 
+  private void notify_is_favourite_cb (Object obj, ParamSpec ps)
+    {
+      this.update_is_favourite ();
+    }
+
   /**
    * Create a new Individual.
    *
@@ -315,6 +354,7 @@ public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
 
       this.update_groups ();
       this.update_presence ();
+      this.update_is_favourite ();
       this.update_avatar ();
     }
 
@@ -405,6 +445,23 @@ public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
         this.presence_type = presence_type;
     }
 
+  private void update_is_favourite ()
+    {
+      bool favourite = false;
+
+      this._personas.foreach ((persona) =>
+        {
+          var p = (Persona) persona;
+
+          if (favourite == false)
+            favourite = p.is_favourite;
+        });
+
+      /* Only notify if the value has changed */
+      if (this._is_favourite != favourite)
+        this._is_favourite = favourite;
+    }
+
   private void update_avatar ()
     {
       File avatar = null;
index eebc3a6..bf43091 100644 (file)
@@ -60,6 +60,11 @@ public abstract class Folks.Persona : Object, Alias, Avatar, Capabilities,
   public abstract string presence_message { get; set; }
 
   /**
+   * {@inheritDoc}
+   */
+  public abstract bool is_favourite { get; set; }
+
+  /**
    * The internal ID used to represent the Persona within its {@link Backend}.
    *
    * This should not be used by client code.