[epiphany/wip/sync: 1/14] sync: Implement open tabs sync



commit 3bd82e484c69b4adeac1ac32846447c33521a419
Author: Gabriel Ivascu <ivascu gabriel59 gmail com>
Date:   Wed Jun 7 01:10:44 2017 +0300

    sync: Implement open tabs sync

 data/org.gnome.epiphany.gschema.xml     |   10 +
 lib/ephy-prefs.h                        |    2 +
 lib/sync/ephy-open-tabs-manager.c       |  257 ++++++++++++++++++++++
 lib/sync/ephy-open-tabs-manager.h       |   38 ++++
 lib/sync/ephy-open-tabs-record.c        |  300 ++++++++++++++++++++++++++
 lib/sync/ephy-open-tabs-record.h        |   41 ++++
 lib/sync/ephy-sync-service.c            |    7 +-
 lib/sync/meson.build                    |    2 +
 po/POTFILES.in                          |    2 +
 src/ephy-shell.c                        |   18 ++
 src/ephy-shell.h                        |    3 +
 src/meson.build                         |    1 +
 src/prefs-dialog.c                      |   74 +++++--
 src/resources/epiphany.gresource.xml    |    1 +
 src/resources/gtk/prefs-dialog.ui       |   36 +++-
 src/resources/gtk/synced-tabs-dialog.ui |   92 ++++++++
 src/synced-tabs-dialog.c                |  355 +++++++++++++++++++++++++++++++
 src/synced-tabs-dialog.h                |   36 +++
 18 files changed, 1255 insertions(+), 20 deletions(-)
---
diff --git a/data/org.gnome.epiphany.gschema.xml b/data/org.gnome.epiphany.gschema.xml
index e354664..e476eba 100644
--- a/data/org.gnome.epiphany.gschema.xml
+++ b/data/org.gnome.epiphany.gschema.xml
@@ -337,6 +337,16 @@
                        <summary>Initial sync or normal sync</summary>
                        <description>TRUE if history collection needs to be synced for the first time, FALSE 
otherwise.</description>
                </key>
+               <key type="b" name="sync-open-tabs-enabled">
+                       <default>false</default>
+                       <summary>Enable open tabs sync</summary>
+                       <description>TRUE if open tabs collection should be synced, FALSE 
otherwise.</description>
+               </key>
+               <key type="d" name="sync-open-tabs-time">
+                       <default>0</default>
+                       <summary>Open tabs sync timestamp</summary>
+                       <description>The timestamp at which last open tabs sync was made.</description>
+               </key>
        </schema>
        <enum id="org.gnome.Epiphany.Permission">
                <value nick="undecided" value="-1"/>
diff --git a/lib/ephy-prefs.h b/lib/ephy-prefs.h
index fde68f9..2cf31c2 100644
--- a/lib/ephy-prefs.h
+++ b/lib/ephy-prefs.h
@@ -165,6 +165,8 @@ static const char * const ephy_prefs_web_schema[] = {
 #define EPHY_PREFS_SYNC_HISTORY_ENABLED   "sync-history-enabled"
 #define EPHY_PREFS_SYNC_HISTORY_TIME      "sync-history-time"
 #define EPHY_PREFS_SYNC_HISTORY_INITIAL   "sync-history-initial"
+#define EPHY_PREFS_SYNC_OPEN_TABS_ENABLED "sync-open-tabs-enabled"
+#define EPHY_PREFS_SYNC_OPEN_TABS_TIME    "sync-open-tabs-time"
 
 static struct {
   const char *schema;
diff --git a/lib/sync/ephy-open-tabs-manager.c b/lib/sync/ephy-open-tabs-manager.c
new file mode 100644
index 0000000..b9d7317
--- /dev/null
+++ b/lib/sync/ephy-open-tabs-manager.c
@@ -0,0 +1,257 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "ephy-open-tabs-manager.h"
+
+#include "ephy-embed-container.h"
+#include "ephy-embed-shell.h"
+#include "ephy-settings.h"
+#include "ephy-synchronizable-manager.h"
+
+struct _EphyOpenTabsManager {
+  GObject parent_instance;
+
+  /* A list of EphyOpenTabsRecord objects describing the open tabs
+   * of other sync clients. This is updated at every sync. */
+  GSList *remote_records;
+};
+
+static void ephy_synchronizable_manager_iface_init (EphySynchronizableManagerInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphyOpenTabsManager, ephy_open_tabs_manager, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (EPHY_TYPE_SYNCHRONIZABLE_MANAGER,
+                                                ephy_synchronizable_manager_iface_init))
+
+static void
+ephy_open_tabs_manager_dispose (GObject *object)
+{
+  EphyOpenTabsManager *self = EPHY_OPEN_TABS_MANAGER (object);
+
+  g_slist_free_full (self->remote_records, g_object_unref);
+  self->remote_records = NULL;
+
+  G_OBJECT_CLASS (ephy_open_tabs_manager_parent_class)->dispose (object);
+}
+
+static void
+ephy_open_tabs_manager_class_init (EphyOpenTabsManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ephy_open_tabs_manager_dispose;
+}
+
+static void
+ephy_open_tabs_manager_init (EphyOpenTabsManager *self)
+{
+}
+
+EphyOpenTabsManager *
+ephy_open_tabs_manager_new (void)
+{
+  return EPHY_OPEN_TABS_MANAGER (g_object_new (EPHY_TYPE_OPEN_TABS_MANAGER, NULL));
+}
+
+EphyOpenTabsRecord *
+ephy_open_tabs_manager_get_local_tabs (EphyOpenTabsManager *self)
+{
+  EphyOpenTabsRecord *local_tabs;
+  WebKitFaviconDatabase *database;
+  EphyEmbedShell *embed_shell;
+  GList *windows;
+  GList *tabs;
+  char *favicon;
+  char *client_id;
+  char *client_name;
+  const char *title;
+  const char *url;
+
+  g_return_val_if_fail (EPHY_IS_OPEN_TABS_MANAGER (self), NULL);
+
+  embed_shell = ephy_embed_shell_get_default ();
+  windows = gtk_application_get_windows (GTK_APPLICATION (embed_shell));
+  database = webkit_web_context_get_favicon_database (ephy_embed_shell_get_web_context (embed_shell));
+  client_id = g_settings_get_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_CLIENT_ID);
+  client_name = g_strdup_printf ("%s on Epiphany", client_id);
+  local_tabs = ephy_open_tabs_record_new (client_id, client_name);
+
+  for (GList *l = windows; l && l->data; l = l->next) {
+    tabs = ephy_embed_container_get_children (l->data);
+
+    for (GList *t = tabs; t && t->data; t = t->next) {
+      title = ephy_embed_get_title (t->data);
+
+      if (!g_strcmp0 (title, "Blank page") || !g_strcmp0 (title, "Most Visited"))
+        continue;
+
+      url = ephy_web_view_get_display_address (ephy_embed_get_web_view (t->data));
+      favicon = webkit_favicon_database_get_favicon_uri (database, url);
+      ephy_open_tabs_record_add_tab (local_tabs, title, url, favicon);
+
+      g_free (favicon);
+    }
+
+    g_list_free (tabs);
+  }
+
+  g_free (client_id);
+  g_free (client_name);
+
+  return local_tabs;
+}
+
+GSList *
+ephy_open_tabs_manager_get_remote_tabs (EphyOpenTabsManager *self)
+{
+  g_return_val_if_fail (EPHY_IS_OPEN_TABS_MANAGER (self), NULL);
+
+  return self->remote_records;
+}
+
+void
+ephy_open_tabs_manager_clear_cache (EphyOpenTabsManager *self)
+{
+  g_return_if_fail (EPHY_IS_OPEN_TABS_MANAGER (self));
+
+  g_slist_free_full (self->remote_records, g_object_unref);
+  self->remote_records = NULL;
+}
+
+const char *
+synchronizable_manager_get_collection_name (EphySynchronizableManager *manager)
+{
+  gboolean sync_with_firefox = g_settings_get_boolean (EPHY_SETTINGS_SYNC,
+                                                       EPHY_PREFS_SYNC_WITH_FIREFOX);
+
+  return sync_with_firefox ? "tabs" : "ephy-tabs";
+}
+
+static GType
+synchronizable_manager_get_synchronizable_type (EphySynchronizableManager *manager)
+{
+  return EPHY_TYPE_OPEN_TABS_RECORD;
+}
+
+static gboolean
+synchronizable_manager_is_initial_sync (EphySynchronizableManager *manager)
+{
+  /* Initial sync will always be true.
+   * We always want all records when syncing open tabs.
+   */
+  return TRUE;
+}
+
+static void
+synchronizable_manager_set_is_initial_sync (EphySynchronizableManager *manager,
+                                            gboolean                   is_initial)
+{
+  /* Initial sync will always be true. */
+}
+
+static double
+synchronizable_manager_get_sync_time (EphySynchronizableManager *manager)
+{
+  return g_settings_get_double (EPHY_SETTINGS_SYNC,
+                                EPHY_PREFS_SYNC_OPEN_TABS_TIME);
+}
+
+static void
+synchronizable_manager_set_sync_time (EphySynchronizableManager *manager,
+                                      double                     sync_time)
+{
+  g_settings_set_double (EPHY_SETTINGS_SYNC,
+                         EPHY_PREFS_SYNC_OPEN_TABS_TIME,
+                         sync_time);
+}
+
+static void
+synchronizable_manager_add (EphySynchronizableManager *manager,
+                            EphySynchronizable        *synchronizable)
+{
+  /* Every sync of open tabs is an initial sync so we don't need this. */
+}
+
+static void
+synchronizable_manager_remove (EphySynchronizableManager *manager,
+                               EphySynchronizable        *synchronizable)
+{
+  /* Every sync of open tabs is an initial sync so we don't need this. */
+}
+
+static void
+synchronizable_manager_save (EphySynchronizableManager *manager,
+                             EphySynchronizable        *synchronizable)
+{
+  /* No implementation.
+   * We don't care about the server time modified of open tabs records.
+   */
+}
+
+static void
+synchronizable_manager_merge (EphySynchronizableManager              *manager,
+                              gboolean                                is_initial,
+                              GSList                                 *remotes_deleted,
+                              GSList                                 *remotes_updated,
+                              EphySynchronizableManagerMergeCallback  callback,
+                              gpointer                                user_data)
+{
+  EphyOpenTabsManager *self = EPHY_OPEN_TABS_MANAGER (manager);
+  EphyOpenTabsRecord *local_tabs;
+  GSList *to_upload = NULL;
+  char *client_id;
+
+  client_id = g_settings_get_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_CLIENT_ID);
+  g_slist_free_full (self->remote_records, g_object_unref);
+  self->remote_records = NULL;
+
+  for (GSList *l = remotes_updated; l && l->data; l = l->next) {
+    /* Exclude the record which describes the local open tabs. */
+    if (!g_strcmp0 (client_id, ephy_open_tabs_record_get_id (l->data)))
+      continue;
+
+    self->remote_records = g_slist_prepend (self->remote_records, g_object_ref (l->data));
+  }
+
+  /* Only upload the local open tabs, we don't want to alter open tabs of
+   * other clients. Also, overwrite any previous value by doing a force upload.
+   */
+  local_tabs = ephy_open_tabs_manager_get_local_tabs (self);
+  to_upload = g_slist_prepend (to_upload, local_tabs);
+
+  g_free (client_id);
+
+  callback (to_upload, TRUE, user_data);
+}
+
+static void
+ephy_synchronizable_manager_iface_init (EphySynchronizableManagerInterface *iface)
+{
+  iface->get_collection_name = synchronizable_manager_get_collection_name;
+  iface->get_synchronizable_type = synchronizable_manager_get_synchronizable_type;
+  iface->is_initial_sync = synchronizable_manager_is_initial_sync;
+  iface->set_is_initial_sync = synchronizable_manager_set_is_initial_sync;
+  iface->get_sync_time = synchronizable_manager_get_sync_time;
+  iface->set_sync_time = synchronizable_manager_set_sync_time;
+  iface->add = synchronizable_manager_add;
+  iface->remove = synchronizable_manager_remove;
+  iface->save = synchronizable_manager_save;
+  iface->merge = synchronizable_manager_merge;
+}
diff --git a/lib/sync/ephy-open-tabs-manager.h b/lib/sync/ephy-open-tabs-manager.h
new file mode 100644
index 0000000..166539e
--- /dev/null
+++ b/lib/sync/ephy-open-tabs-manager.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ephy-open-tabs-record.h"
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_OPEN_TABS_MANAGER (ephy_open_tabs_manager_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyOpenTabsManager, ephy_open_tabs_manager, EPHY, OPEN_TABS_MANAGER, GObject)
+
+EphyOpenTabsManager *ephy_open_tabs_manager_new             (void);
+EphyOpenTabsRecord  *ephy_open_tabs_manager_get_local_tabs  (EphyOpenTabsManager *self);
+GSList              *ephy_open_tabs_manager_get_remote_tabs (EphyOpenTabsManager *self);
+void                 ephy_open_tabs_manager_clear_cache     (EphyOpenTabsManager *self);
+
+G_END_DECLS
diff --git a/lib/sync/ephy-open-tabs-record.c b/lib/sync/ephy-open-tabs-record.c
new file mode 100644
index 0000000..c10a7ec
--- /dev/null
+++ b/lib/sync/ephy-open-tabs-record.c
@@ -0,0 +1,300 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "ephy-open-tabs-record.h"
+
+#include "ephy-synchronizable.h"
+
+struct _EphyOpenTabsRecord {
+  GObject parent_instance;
+
+  char  *id;
+  char  *client_name;
+
+  /* List of JSON objects. Each object describes a tab and has the fields:
+   * @title: a string representing the title of the current page
+   * @urlHistory: a JSON array of strings (the page URLs in tab's history)
+   * @icon: a string representing the favicon URI of the tab, i.e. the favicon
+   *        URI of the most recent website in tab (the first item in urlHistory)
+   * @lastUsed: an integer representing the UNIX time in seconds at which the
+   *            tab was last accessed, or 0
+   */
+  GSList *tabs;
+};
+
+static void json_serializable_iface_init (JsonSerializableIface *iface);
+static void ephy_synchronizable_iface_init (EphySynchronizableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphyOpenTabsRecord, ephy_open_tabs_record, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (JSON_TYPE_SERIALIZABLE,
+                                                json_serializable_iface_init)
+                         G_IMPLEMENT_INTERFACE (EPHY_TYPE_SYNCHRONIZABLE,
+                                                ephy_synchronizable_iface_init))
+
+enum {
+  PROP_0,
+  PROP_ID,
+  PROP_CLIENT_NAME,
+  PROP_TABS,
+  LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+static void
+ephy_open_tabs_record_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  EphyOpenTabsRecord *self = EPHY_OPEN_TABS_RECORD (object);
+
+  switch (prop_id) {
+    case PROP_ID:
+      g_free (self->id);
+      self->id = g_strdup (g_value_get_string (value));
+      break;
+    case PROP_CLIENT_NAME:
+      g_free (self->client_name);
+      self->client_name = g_strdup (g_value_get_string (value));
+      break;
+    case PROP_TABS:
+      g_slist_free_full (self->tabs, (GDestroyNotify)json_object_unref);
+      self->tabs = g_value_get_pointer (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+ephy_open_tabs_record_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  EphyOpenTabsRecord *self = EPHY_OPEN_TABS_RECORD (object);
+
+  switch (prop_id) {
+    case PROP_ID:
+      g_value_set_string (value, self->id);
+      break;
+    case PROP_CLIENT_NAME:
+      g_value_set_string (value, self->client_name);
+      break;
+    case PROP_TABS:
+      g_value_set_pointer (value, self->tabs);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+ephy_open_tabs_record_dispose (GObject *object)
+{
+  EphyOpenTabsRecord *self = EPHY_OPEN_TABS_RECORD (object);
+
+  g_clear_pointer (&self->id, g_free);
+  g_clear_pointer (&self->client_name, g_free);
+  g_slist_free_full (self->tabs, (GDestroyNotify)json_object_unref);
+  self->tabs = NULL;
+
+  G_OBJECT_CLASS (ephy_open_tabs_record_parent_class)->dispose (object);
+}
+
+static void
+ephy_open_tabs_record_class_init (EphyOpenTabsRecordClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->set_property = ephy_open_tabs_record_set_property;
+  object_class->get_property = ephy_open_tabs_record_get_property;
+  object_class->dispose = ephy_open_tabs_record_dispose;
+
+  obj_properties[PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "Id of the open tabs record",
+                         "Default id",
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+  obj_properties[PROP_CLIENT_NAME] =
+    g_param_spec_string ("clientName",
+                         "Client name",
+                         "Name of the sync client providing the tabs",
+                         "Default client name",
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+  obj_properties[PROP_TABS] =
+    g_param_spec_pointer ("tabs",
+                          "Tabs",
+                          "A list of JSON objects describing the tabs",
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+}
+
+static void
+ephy_open_tabs_record_init (EphyOpenTabsRecord *self)
+{
+}
+
+EphyOpenTabsRecord *
+ephy_open_tabs_record_new (const char *id,
+                           const char *client_name)
+{
+  return EPHY_OPEN_TABS_RECORD (g_object_new (EPHY_TYPE_OPEN_TABS_RECORD,
+                                              "id", id,
+                                              "clientName", client_name,
+                                              NULL));
+}
+
+const char *
+ephy_open_tabs_record_get_id (EphyOpenTabsRecord *self)
+{
+  g_return_val_if_fail (EPHY_IS_OPEN_TABS_RECORD (self), NULL);
+
+  return self->id;
+}
+
+const char *
+ephy_open_tabs_record_get_client_name (EphyOpenTabsRecord *self)
+{
+  g_return_val_if_fail (EPHY_IS_OPEN_TABS_RECORD (self), NULL);
+
+  return self->client_name;
+}
+
+GSList *
+ephy_open_tabs_record_get_tabs (EphyOpenTabsRecord *self)
+{
+  g_return_val_if_fail (EPHY_IS_OPEN_TABS_RECORD (self), NULL);
+
+  return self->tabs;
+}
+
+void
+ephy_open_tabs_record_add_tab (EphyOpenTabsRecord *self,
+                               const char         *title,
+                               const char         *url,
+                               const char         *favicon)
+{
+  JsonObject *tab;
+  JsonArray *url_history;
+
+  g_return_if_fail (EPHY_IS_OPEN_TABS_RECORD (self));
+  g_return_if_fail (title);
+  g_return_if_fail (url);
+
+  tab = json_object_new ();
+  json_object_set_string_member (tab, "title", title);
+  /* Only use the most recent URL. */
+  url_history = json_array_new ();
+  json_array_add_string_element (url_history, url);
+  json_object_set_array_member (tab, "urlHistory", url_history);
+  json_object_set_string_member (tab, "icon", favicon);
+  json_object_set_int_member (tab, "lastUsed", g_get_real_time () / 1000000);
+
+  self->tabs = g_slist_prepend (self->tabs, tab);
+}
+
+static JsonNode *
+serializable_serialize_property (JsonSerializable *serializable,
+                                 const char       *name,
+                                 const GValue     *value,
+                                 GParamSpec       *pspec)
+{
+  if (!g_strcmp0 (name, "tabs")) {
+    JsonNode *node = json_node_new (JSON_NODE_ARRAY);
+    JsonArray *array = json_array_new ();
+
+    for (GList *l = g_value_get_pointer (value); l && l->data; l = l->next)
+      json_array_add_object_element (array, json_object_ref (l->data));
+
+    json_node_set_array (node, array);
+
+    return node;
+  }
+
+  return json_serializable_default_serialize_property (serializable, name, value, pspec);
+}
+
+static gboolean
+serializable_deserialize_property (JsonSerializable *serializable,
+                                   const char       *name,
+                                   GValue           *value,
+                                   GParamSpec       *pspec,
+                                   JsonNode         *node)
+{
+  if (!g_strcmp0 (name, "tabs")) {
+    JsonArray *array;
+    GSList *tabs = NULL;
+
+    array = json_node_get_array (node);
+    for (guint i = 0; i < json_array_get_length (array); i++)
+      tabs = g_slist_prepend (tabs, json_object_ref (json_array_get_object_element (array, i)));
+
+    g_value_set_pointer (value, tabs);
+
+    return TRUE;
+  }
+
+  return json_serializable_default_deserialize_property (serializable, name, value, pspec, node);
+}
+
+static void
+json_serializable_iface_init (JsonSerializableIface *iface)
+{
+  iface->serialize_property = serializable_serialize_property;
+  iface->deserialize_property = serializable_deserialize_property;
+}
+
+static const char *
+synchronizable_get_id (EphySynchronizable *synchronizable)
+{
+  return EPHY_OPEN_TABS_RECORD (synchronizable)->id;
+}
+
+static double
+synchronizable_get_server_time_modified (EphySynchronizable *synchronizable)
+{
+  /* No implementation.
+   * We don't care about the server time modified of open tabs records.
+   */
+  return 0;
+}
+
+static void
+synchronizable_set_server_time_modified (EphySynchronizable *synchronizable,
+                                         double              server_time_modified)
+{
+  /* No implementation.
+   * We don't care about the server time modified of open tabs records.
+   */
+}
+
+static void
+ephy_synchronizable_iface_init (EphySynchronizableInterface *iface)
+{
+  iface->get_id = synchronizable_get_id;
+  iface->get_server_time_modified = synchronizable_get_server_time_modified;
+  iface->set_server_time_modified = synchronizable_set_server_time_modified;
+  iface->to_bso = ephy_synchronizable_default_to_bso;
+}
diff --git a/lib/sync/ephy-open-tabs-record.h b/lib/sync/ephy-open-tabs-record.h
new file mode 100644
index 0000000..b31fc82
--- /dev/null
+++ b/lib/sync/ephy-open-tabs-record.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_OPEN_TABS_RECORD (ephy_open_tabs_record_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyOpenTabsRecord, ephy_open_tabs_record, EPHY, OPEN_TABS_RECORD, GObject)
+
+EphyOpenTabsRecord *ephy_open_tabs_record_new             (const char *id,
+                                                           const char *client_name);
+const char         *ephy_open_tabs_record_get_id          (EphyOpenTabsRecord *self);
+const char         *ephy_open_tabs_record_get_client_name (EphyOpenTabsRecord *self);
+GSList             *ephy_open_tabs_record_get_tabs        (EphyOpenTabsRecord *self);
+void                ephy_open_tabs_record_add_tab         (EphyOpenTabsRecord *self,
+                                                           const char         *title,
+                                                           const char         *url,
+                                                           const char         *favicon);
+
+G_END_DECLS
diff --git a/lib/sync/ephy-sync-service.c b/lib/sync/ephy-sync-service.c
index d1f299c..fea3ad1 100644
--- a/lib/sync/ephy-sync-service.c
+++ b/lib/sync/ephy-sync-service.c
@@ -999,7 +999,7 @@ ephy_sync_service_delete_synchronizable (EphySyncService           *self,
   json_object_set_boolean_member (object, "deleted", TRUE);
   record = json_to_string (node, FALSE);
   bundle = ephy_sync_service_get_key_bundle (self, collection);
-  payload = ephy_sync_crypto_encrypt_record (record,  bundle);
+  payload = ephy_sync_crypto_encrypt_record (record, bundle);
   json_object_remove_member (object, "deleted");
   json_object_set_string_member (object, "payload", payload);
   body = json_to_string (node, FALSE);
@@ -1337,6 +1337,10 @@ ephy_sync_service_unregister_client_id (EphySyncService *self)
 
   ephy_sync_service_queue_storage_request (self, endpoint, SOUP_METHOD_DELETE,
                                            NULL, -1, -1, NULL, NULL);
+  g_free (endpoint);
+  endpoint = g_strdup_printf ("storage/tabs/%s", client_id);
+  ephy_sync_service_queue_storage_request (self, endpoint, SOUP_METHOD_DELETE,
+                                           NULL, -1, -1, NULL, NULL);
   g_settings_set_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_CLIENT_ID, "");
 
   g_free (endpoint);
@@ -1897,7 +1901,6 @@ ephy_sync_service_upload_meta_global_record (EphySyncService *self)
   declined = json_array_new ();
   json_array_add_string_element (declined, "addons");
   json_array_add_string_element (declined, "prefs");
-  json_array_add_string_element (declined, "tabs");
   json_object_set_array_member (payload, "declined", declined);
   json_object_set_object_member (engines, "clients", make_engine_object (1));
   json_object_set_object_member (engines, "bookmarks", make_engine_object (2));
diff --git a/lib/sync/meson.build b/lib/sync/meson.build
index 728ab12..ae6f72d 100644
--- a/lib/sync/meson.build
+++ b/lib/sync/meson.build
@@ -2,6 +2,8 @@ libephysync_sources = [
   'debug/ephy-sync-debug.c',
   'ephy-history-manager.c',
   'ephy-history-record.c',
+  'ephy-open-tabs-manager.c',
+  'ephy-open-tabs-record.c',
   'ephy-password-manager.c',
   'ephy-password-record.c',
   'ephy-sync-crypto.c',
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 04ee4ee..f9308f0 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -60,5 +60,7 @@ src/resources/gtk/prefs-dialog.ui
 src/resources/gtk/prefs-lang-dialog.ui
 src/resources/gtk/search-engine-dialog.ui
 src/resources/gtk/shortcuts-dialog.ui
+src/resources/gtk/synced-tabs-dialog.ui
 src/search-provider/ephy-search-provider.c
+src/synced-tabs-dialog.c
 src/window-commands.c
diff --git a/src/ephy-shell.c b/src/ephy-shell.c
index 886f639..4b70a61 100644
--- a/src/ephy-shell.c
+++ b/src/ephy-shell.c
@@ -58,6 +58,7 @@ struct _EphyShell {
   EphyBookmarksManager *bookmarks_manager;
   EphyPasswordManager *password_manager;
   EphyHistoryManager *history_manager;
+  EphyOpenTabsManager *open_tabs_manager;
   GNetworkMonitor *network_monitor;
   GtkWidget *history_dialog;
   GObject *prefs_dialog;
@@ -363,6 +364,11 @@ ephy_shell_startup (GApplication *application)
         manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager (shell));
         ephy_sync_service_register_manager (ephy_shell_get_sync_service (shell), manager);
       }
+
+      if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_OPEN_TABS_ENABLED)) {
+        manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_open_tabs_manager (shell));
+        ephy_sync_service_register_manager (ephy_shell_get_sync_service (shell), manager);
+      }
     }
 
     gtk_application_set_app_menu (GTK_APPLICATION (application),
@@ -635,6 +641,7 @@ ephy_shell_dispose (GObject *object)
   g_clear_object (&shell->bookmarks_manager);
   g_clear_object (&shell->password_manager);
   g_clear_object (&shell->history_manager);
+  g_clear_object (&shell->open_tabs_manager);
 
   g_slist_free_full (shell->open_uris_idle_ids, remove_open_uris_idle_cb);
   shell->open_uris_idle_ids = NULL;
@@ -868,6 +875,17 @@ ephy_shell_get_history_manager (EphyShell *shell)
   return shell->history_manager;
 }
 
+EphyOpenTabsManager *
+ephy_shell_get_open_tabs_manager (EphyShell *shell)
+{
+  g_return_val_if_fail (EPHY_IS_SHELL (shell), NULL);
+
+  if (shell->open_tabs_manager == NULL)
+    shell->open_tabs_manager = ephy_open_tabs_manager_new ();
+
+  return shell->open_tabs_manager;
+}
+
 /**
  * ephy_shell_get_net_monitor:
  *
diff --git a/src/ephy-shell.h b/src/ephy-shell.h
index b7d4800..b07bb4d 100644
--- a/src/ephy-shell.h
+++ b/src/ephy-shell.h
@@ -26,6 +26,7 @@
 #include "ephy-embed-shell.h"
 #include "ephy-embed.h"
 #include "ephy-history-manager.h"
+#include "ephy-open-tabs-manager.h"
 #include "ephy-password-manager.h"
 #include "ephy-session.h"
 #include "ephy-sync-service.h"
@@ -108,6 +109,8 @@ EphyPasswordManager *ephy_shell_get_password_manager     (EphyShell *shell);
 
 EphyHistoryManager *ephy_shell_get_history_manager       (EphyShell *shell);
 
+EphyOpenTabsManager *ephy_shell_get_open_tabs_manager    (EphyShell *shell);
+
 EphySyncService *ephy_shell_get_sync_service             (EphyShell *shell);
 
 GtkWidget       *ephy_shell_get_history_dialog           (EphyShell *shell);
diff --git a/src/meson.build b/src/meson.build
index a06356b..8c9e97e 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -40,6 +40,7 @@ libephymain_sources = [
   'passwords-dialog.c',
   'popup-commands.c',
   'prefs-dialog.c',
+  'synced-tabs-dialog.c',
   'window-commands.c',
   enums
 ]
diff --git a/src/prefs-dialog.c b/src/prefs-dialog.c
index 75fce2a..8b16caa 100644
--- a/src/prefs-dialog.c
+++ b/src/prefs-dialog.c
@@ -44,6 +44,7 @@
 #include "cookies-dialog.h"
 #include "languages.h"
 #include "passwords-dialog.h"
+#include "synced-tabs-dialog.h"
 
 #include <glib/gi18n.h>
 #include <gtk/gtk.h>
@@ -125,11 +126,13 @@ struct _PrefsDialog {
   GtkWidget *sync_bookmarks_checkbutton;
   GtkWidget *sync_passwords_checkbutton;
   GtkWidget *sync_history_checkbutton;
+  GtkWidget *sync_open_tabs_checkbutton;
   GtkWidget *sync_frequency_5_min_radiobutton;
   GtkWidget *sync_frequency_15_min_radiobutton;
   GtkWidget *sync_frequency_30_min_radiobutton;
   GtkWidget *sync_frequency_60_min_radiobutton;
   GtkWidget *sync_now_button;
+  GtkWidget *synced_tabs_button;
   gboolean sync_was_signed_in;
 
   WebKitWebView *fxa_web_view;
@@ -181,15 +184,20 @@ sync_collection_toggled_cb (GtkToggleButton *button,
                             PrefsDialog     *dialog)
 {
   EphySynchronizableManager *manager = NULL;
-
-  if (GTK_WIDGET (button) == dialog->sync_bookmarks_checkbutton)
-    manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_bookmarks_manager (ephy_shell_get_default ()));
-  else if (GTK_WIDGET (button) == dialog->sync_passwords_checkbutton)
-    manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_password_manager (ephy_shell_get_default ()));
-  else if (GTK_WIDGET (button) == dialog->sync_history_checkbutton)
-    manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager (ephy_shell_get_default ()));
-  else
+  EphyShell *shell = ephy_shell_get_default ();
+
+  if (GTK_WIDGET (button) == dialog->sync_bookmarks_checkbutton) {
+    manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_bookmarks_manager (shell));
+  } else if (GTK_WIDGET (button) == dialog->sync_passwords_checkbutton) {
+    manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_password_manager (shell));
+  } else if (GTK_WIDGET (button) == dialog->sync_history_checkbutton) {
+    manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager (shell));
+  } else if (GTK_WIDGET (button) == dialog->sync_open_tabs_checkbutton) {
+    manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_open_tabs_manager (shell));
+    ephy_open_tabs_manager_clear_cache (EPHY_OPEN_TABS_MANAGER (manager));
+  } else {
     g_assert_not_reached ();
+  }
 
   if (gtk_toggle_button_get_active (button)) {
     ephy_sync_service_register_manager (dialog->sync_service, manager);
@@ -258,9 +266,8 @@ sync_secrets_store_finished_cb (EphySyncService *service,
                                 GError          *error,
                                 PrefsDialog     *dialog)
 {
-  EphyBookmarksManager *bookmarks_manager;
-  EphyPasswordManager *password_manager;
-  EphyHistoryManager *history_manager;
+  EphySynchronizableManager *manager;
+  EphyShell *shell = ephy_shell_get_default ();
 
   g_assert (EPHY_IS_SYNC_SERVICE (service));
   g_assert (EPHY_IS_PREFS_DIALOG (dialog));
@@ -288,16 +295,20 @@ sync_secrets_store_finished_cb (EphySyncService *service,
                            ephy_sync_service_get_sync_user (service));
 
     if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_BOOKMARKS_ENABLED)) {
-      bookmarks_manager = ephy_shell_get_bookmarks_manager (ephy_shell_get_default ());
-      ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (bookmarks_manager));
+      manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_bookmarks_manager (shell));
+      ephy_sync_service_register_manager (service, manager);
     }
     if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_PASSWORDS_ENABLED)) {
-      password_manager = ephy_shell_get_password_manager (ephy_shell_get_default ());
-      ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (password_manager));
+      manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_password_manager (shell));
+      ephy_sync_service_register_manager (service, manager);
     }
     if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_HISTORY_ENABLED)) {
-      history_manager = ephy_shell_get_history_manager (ephy_shell_get_default ());
-      ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (history_manager));
+      manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager (shell));
+      ephy_sync_service_register_manager (service, manager);
+    }
+    if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_OPEN_TABS_ENABLED)) {
+      manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_open_tabs_manager (shell));
+      ephy_sync_service_register_manager (service, manager);
     }
 
     g_free (text);
@@ -551,6 +562,20 @@ on_sync_sync_now_button_clicked (GtkWidget   *button,
 }
 
 static void
+on_sync_synced_tabs_button_clicked (GtkWidget   *button,
+                                    PrefsDialog *dialog)
+{
+  EphyOpenTabsManager *manager;
+  SyncedTabsDialog *synced_tabs_dialog;
+
+  manager = ephy_shell_get_open_tabs_manager (ephy_shell_get_default ());
+  synced_tabs_dialog = synced_tabs_dialog_new (manager);
+  gtk_window_set_transient_for (GTK_WINDOW (synced_tabs_dialog), GTK_WINDOW (dialog));
+  gtk_window_set_modal (GTK_WINDOW (synced_tabs_dialog), TRUE);
+  gtk_window_present (GTK_WINDOW (synced_tabs_dialog));
+}
+
+static void
 on_manage_cookies_button_clicked (GtkWidget   *button,
                                   PrefsDialog *dialog)
 {
@@ -657,17 +682,20 @@ prefs_dialog_class_init (PrefsDialogClass *klass)
   gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_bookmarks_checkbutton);
   gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_passwords_checkbutton);
   gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_history_checkbutton);
+  gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_open_tabs_checkbutton);
   gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_5_min_radiobutton);
   gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_15_min_radiobutton);
   gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_30_min_radiobutton);
   gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_60_min_radiobutton);
   gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_now_button);
+  gtk_widget_class_bind_template_child (widget_class, PrefsDialog, synced_tabs_button);
 
   gtk_widget_class_bind_template_callback (widget_class, on_manage_cookies_button_clicked);
   gtk_widget_class_bind_template_callback (widget_class, on_manage_passwords_button_clicked);
   gtk_widget_class_bind_template_callback (widget_class, on_search_engine_dialog_button_clicked);
   gtk_widget_class_bind_template_callback (widget_class, on_sync_sign_out_button_clicked);
   gtk_widget_class_bind_template_callback (widget_class, on_sync_sync_now_button_clicked);
+  gtk_widget_class_bind_template_callback (widget_class, on_sync_synced_tabs_button_clicked);
 }
 
 static void
@@ -1721,6 +1749,11 @@ setup_sync_page (PrefsDialog *dialog)
                    dialog->sync_history_checkbutton,
                    "active",
                    G_SETTINGS_BIND_DEFAULT);
+  g_settings_bind (sync_settings,
+                   EPHY_PREFS_SYNC_OPEN_TABS_ENABLED,
+                   dialog->sync_open_tabs_checkbutton,
+                   "active",
+                   G_SETTINGS_BIND_DEFAULT);
   g_settings_bind_with_mapping (sync_settings,
                                 EPHY_PREFS_SYNC_FREQUENCY,
                                 dialog->sync_frequency_5_min_radiobutton,
@@ -1758,6 +1791,10 @@ setup_sync_page (PrefsDialog *dialog)
                                 GINT_TO_POINTER (60),
                                 NULL);
 
+  g_object_bind_property (dialog->sync_open_tabs_checkbutton, "active",
+                          dialog->synced_tabs_button, "sensitive",
+                          G_BINDING_SYNC_CREATE);
+
   g_signal_connect_object (dialog->sync_service, "sync-secrets-store-finished",
                            G_CALLBACK (sync_secrets_store_finished_cb),
                            dialog, 0);
@@ -1779,6 +1816,9 @@ setup_sync_page (PrefsDialog *dialog)
   g_signal_connect_object (dialog->sync_history_checkbutton, "toggled",
                            G_CALLBACK (sync_collection_toggled_cb),
                            dialog, 0);
+  g_signal_connect_object (dialog->sync_open_tabs_checkbutton, "toggled",
+                           G_CALLBACK (sync_collection_toggled_cb),
+                           dialog, 0);
 }
 
 static void
diff --git a/src/resources/epiphany.gresource.xml b/src/resources/epiphany.gresource.xml
index 8580f8f..80c9e7d 100644
--- a/src/resources/epiphany.gresource.xml
+++ b/src/resources/epiphany.gresource.xml
@@ -27,6 +27,7 @@
     <file preprocess="xml-stripblanks" compressed="true">gtk/prefs-dialog.ui</file>
     <file preprocess="xml-stripblanks" compressed="true">gtk/prefs-lang-dialog.ui</file>
     <file preprocess="xml-stripblanks" compressed="true">gtk/search-engine-dialog.ui</file>
+    <file preprocess="xml-stripblanks" compressed="true">gtk/synced-tabs-dialog.ui</file>
     <file preprocess="xml-stripblanks" compressed="true">gtk/shortcuts-dialog.ui</file>
   </gresource>
   <gresource prefix="/org/gnome/Epiphany/icons">
diff --git a/src/resources/gtk/prefs-dialog.ui b/src/resources/gtk/prefs-dialog.ui
index 1db23d1..ac32223 100644
--- a/src/resources/gtk/prefs-dialog.ui
+++ b/src/resources/gtk/prefs-dialog.ui
@@ -905,7 +905,7 @@
                         <child>
                           <object class="GtkBox">
                             <property name="visible">True</property>
-                            <property name="orientation">vertical</property>
+                            <property name="orientation">horizontal</property>
                             <property name="spacing">6</property>
                             <property name="margin-start">12</property>
                             <child>
@@ -929,6 +929,13 @@
                                 <property name="use-underline">True</property>
                               </object>
                             </child>
+                            <child>
+                              <object class="GtkCheckButton" id="sync_open_tabs_checkbutton">
+                                <property name="label" translatable="yes">Open _Tabs</property>
+                                <property name="visible">True</property>
+                                <property name="use-underline">True</property>
+                              </object>
+                            </child>
                           </object>
                         </child>
                         <child>
@@ -994,6 +1001,33 @@
                             </child>
                           </object>
                         </child>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="halign">start</property>
+                            <property name="label" translatable="yes">Misc</property>
+                            <attributes>
+                              <attribute name="weight" value="bold"/>
+                            </attributes>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">6</property>
+                            <property name="margin-start">12</property>
+                            <child>
+                              <object class="GtkButton" id="synced_tabs_button">
+                                <property name="label" translatable="yes">_View synced tabs</property>
+                                <property name="visible">True</property>
+                                <property name="use-underline">True</property>
+                                <property name="halign">start</property>
+                                <signal name="clicked" handler="on_sync_synced_tabs_button_clicked"/>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
                       </object>
                     </child>
                   </object>
diff --git a/src/resources/gtk/synced-tabs-dialog.ui b/src/resources/gtk/synced-tabs-dialog.ui
new file mode 100644
index 0000000..86410f2
--- /dev/null
+++ b/src/resources/gtk/synced-tabs-dialog.ui
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <object class="GtkTreeStore" id="treestore">
+    <columns>
+      <!-- column-name ICON -->
+      <column type="GdkPixbuf"/>
+      <!-- column-name TITLE -->
+      <column type="gchararray"/>
+      <!-- column-name URL -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <template class="SyncedTabsDialog" parent="GtkDialog">
+    <property name="height_request">500</property>
+    <property name="modal">True</property>
+    <property name="window_position">center</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="type_hint">dialog</property>
+    <child internal-child="headerbar">
+      <object class="GtkHeaderBar">
+        <property name="title" translatable="yes">Synced Tabs</property>
+        <property name="show-close-button">True</property>
+      </object>
+    </child>
+    <child internal-child="vbox">
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="border_width">15</property>
+        <property name="spacing">12</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <property name="wrap">True</property>
+            <property name="max-width-chars">50</property>
+            <property name="label" translatable="yes">Below are the synced open tabs of your other devices 
that use Firefox Sync with this account. Except for the tabs under Local Tabs, all the other tabs can be 
opened by double clicking on their name.</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="visible">True</property>
+                <property name="expand">True</property>
+                <child>
+                  <object class="GtkTreeView" id="treeview">
+                    <property name="visible">True</property>
+                    <property name="model">treestore</property>
+                    <property name="headers-visible">False</property>
+                    <signal name="row-activated" handler="treeview_row_activated_cb"/>
+                    <child internal-child="selection">
+                      <object class="GtkTreeSelection">
+                        <property name="mode">single</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <child>
+                          <object class="GtkCellRendererPixbuf"/>
+                          <attributes>
+                            <attribute name="pixbuf">0</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <child>
+                          <object class="GtkCellRendererText">
+                            <property name="ellipsize">end</property>
+                          </object>
+                          <attributes>
+                            <attribute name="text">1</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/synced-tabs-dialog.c b/src/synced-tabs-dialog.c
new file mode 100644
index 0000000..d6cd8eb
--- /dev/null
+++ b/src/synced-tabs-dialog.c
@@ -0,0 +1,355 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "synced-tabs-dialog.h"
+
+#include "ephy-embed-prefs.h"
+#include "ephy-embed-shell.h"
+#include "ephy-favicon-helpers.h"
+#include "ephy-shell.h"
+
+#include <json-glib/json-glib.h>
+
+#define PIXBUF_MISSING_PATH "/org/gnome/epiphany/missing-thumbnail.png"
+
+struct _SyncedTabsDialog {
+  GtkDialog parent_instance;
+
+  EphyOpenTabsManager *manager;
+
+  WebKitFaviconDatabase *database;
+  GdkPixbuf *pixbuf_root;
+  GdkPixbuf *pixbuf_missing;
+
+  GtkTreeModel *treestore;
+  GtkWidget *treeview;
+};
+
+G_DEFINE_TYPE (SyncedTabsDialog, synced_tabs_dialog, GTK_TYPE_DIALOG)
+
+enum {
+  ICON_COLUMN,
+  TITLE_COLUMN,
+  URL_COLUMN
+};
+
+enum {
+  PROP_0,
+  PROP_OPEN_TABS_MANAGER,
+  LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+typedef struct {
+  SyncedTabsDialog *dialog;
+  char             *title;
+  char             *url;
+  guint             parent_index;
+} PopulateRowAsyncData;
+
+static PopulateRowAsyncData *
+populate_row_async_data_new (SyncedTabsDialog *dialog,
+                             const char       *title,
+                             const char       *url,
+                             guint             parent_index)
+{
+  PopulateRowAsyncData *data;
+
+  data = g_slice_new (PopulateRowAsyncData);
+  data->dialog = g_object_ref (dialog);
+  data->title = g_strdup (title);
+  data->url = g_strdup (url);
+  data->parent_index = parent_index;
+
+  return data;
+}
+
+static void
+populate_row_async_data_free (PopulateRowAsyncData *data)
+{
+  g_object_unref (data->dialog);
+  g_free (data->title);
+  g_free (data->url);
+  g_slice_free (PopulateRowAsyncData, data);
+}
+
+static void
+treeview_row_activated_cb (GtkTreeView       *view,
+                           GtkTreePath       *path,
+                           GtkTreeViewColumn *column,
+                           gpointer           user_data)
+{
+  EphyShell *shell;
+  EphyEmbed *embed;
+  GtkWindow *window;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+  char *url;
+  char *path_str;
+
+  /* No action on top-level rows. */
+  if (gtk_tree_path_get_depth (path) == 1)
+    return;
+
+  /* No action on local tabs, i.e. children of first top-level row. */
+  path_str = gtk_tree_path_to_string (path);
+  if (g_str_has_prefix (path_str, "0:"))
+    goto out;
+
+  model = gtk_tree_view_get_model (view);
+  gtk_tree_model_get_iter (model, &iter, path);
+  gtk_tree_model_get (model, &iter, URL_COLUMN, &url, -1);
+
+  shell = ephy_shell_get_default ();
+  window = gtk_application_get_active_window (GTK_APPLICATION (shell));
+  embed = ephy_shell_new_tab (shell, EPHY_WINDOW (window),
+                              NULL, EPHY_NEW_TAB_APPEND_LAST);
+  ephy_web_view_load_url (ephy_embed_get_web_view (embed), url);
+
+  g_free (url);
+out:
+  g_free (path_str);
+}
+
+static void
+synced_tabs_dialog_favicon_loaded_cb (GObject      *source,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  WebKitFaviconDatabase *database = WEBKIT_FAVICON_DATABASE (source);
+  PopulateRowAsyncData *data = (PopulateRowAsyncData *)user_data;
+  cairo_surface_t *surface;
+  GdkPixbuf *favicon = NULL;
+  GtkTreeIter parent_iter;
+  char *escaped_url;
+
+  surface = webkit_favicon_database_get_favicon_finish (database, result, NULL);
+  if (surface) {
+    favicon = ephy_pixbuf_get_from_surface_scaled (surface, FAVICON_SIZE, FAVICON_SIZE);
+    cairo_surface_destroy (surface);
+  }
+
+  gtk_tree_model_get_iter_first (data->dialog->treestore, &parent_iter);
+  for (guint i = 0; i < data->parent_index; i++)
+    gtk_tree_model_iter_next (data->dialog->treestore, &parent_iter);
+
+  favicon = favicon ? favicon : data->dialog->pixbuf_missing;
+  escaped_url = g_markup_escape_text (data->url, -1);
+  gtk_tree_store_insert_with_values (GTK_TREE_STORE (data->dialog->treestore),
+                                     NULL, &parent_iter, -1,
+                                     ICON_COLUMN, favicon,
+                                     TITLE_COLUMN, data->title,
+                                     URL_COLUMN, escaped_url,
+                                     -1);
+
+  g_free (escaped_url);
+  populate_row_async_data_free (data);
+}
+
+static void
+synced_tabs_dialog_populate_from_record (SyncedTabsDialog   *dialog,
+                                         EphyOpenTabsRecord *record,
+                                         gboolean            is_local,
+                                         guint               index)
+{
+  PopulateRowAsyncData *data;
+  JsonArray *url_history;
+  GSList *tabs;
+  const char *title;
+  const char *url;
+
+  g_assert (EPHY_IS_SYNCED_TABS_DIALOG (dialog));
+  g_assert (EPHY_IS_OPEN_TABS_RECORD (record));
+
+  if (is_local)
+    title = _("Local Tabs");
+  else
+    title = ephy_open_tabs_record_get_client_name (record);
+
+  /* Insert top-level row. */
+  gtk_tree_store_insert_with_values (GTK_TREE_STORE (dialog->treestore),
+                                     NULL, NULL, -1,
+                                     ICON_COLUMN, dialog->pixbuf_root,
+                                     TITLE_COLUMN, title,
+                                     URL_COLUMN, NULL,
+                                     -1);
+
+  tabs = ephy_open_tabs_record_get_tabs (record);
+  for (GSList *l = tabs; l && l->data; l = l->next) {
+    title = json_object_get_string_member (l->data, "title");
+    url_history = json_object_get_array_member (l->data, "urlHistory");
+    url = json_array_get_string_element (url_history, 0);
+
+    data = populate_row_async_data_new (dialog, title, url, index);
+    webkit_favicon_database_get_favicon (dialog->database, url, NULL,
+                                         synced_tabs_dialog_favicon_loaded_cb,
+                                         data);
+  }
+}
+
+static void
+synced_tabs_dialog_populate_model (SyncedTabsDialog *dialog)
+{
+  EphyOpenTabsRecord *record;
+  GSList *remotes;
+  guint index = 0;
+
+  /* Insert local tabs. */
+  record = ephy_open_tabs_manager_get_local_tabs (dialog->manager);
+  synced_tabs_dialog_populate_from_record (dialog, record, TRUE, index++);
+
+  /* Insert remote tabs. */
+  remotes = ephy_open_tabs_manager_get_remote_tabs (dialog->manager);
+  for (GSList *l = remotes; l && l->data; l = l->next)
+    synced_tabs_dialog_populate_from_record (dialog, l->data, FALSE, index++);
+
+  g_object_unref (record);
+}
+
+static void
+synced_tabs_dialog_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  SyncedTabsDialog *dialog = EPHY_SYNCED_TABS_DIALOG (object);
+
+  switch (prop_id) {
+    case PROP_OPEN_TABS_MANAGER:
+      g_clear_object (&dialog->manager);
+      dialog->manager = g_object_ref (g_value_get_object (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+synced_tabs_dialog_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  SyncedTabsDialog *dialog = EPHY_SYNCED_TABS_DIALOG (object);
+
+  switch (prop_id) {
+    case PROP_OPEN_TABS_MANAGER:
+      g_value_set_object (value, dialog->manager);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+synced_tabs_dialog_constructed (GObject *object)
+{
+  SyncedTabsDialog *dialog = EPHY_SYNCED_TABS_DIALOG (object);
+
+  G_OBJECT_CLASS (synced_tabs_dialog_parent_class)->constructed (object);
+
+  synced_tabs_dialog_populate_model (dialog);
+}
+
+static void
+synced_tabs_dialog_dispose (GObject *object)
+{
+  SyncedTabsDialog *dialog = EPHY_SYNCED_TABS_DIALOG (object);
+
+  g_clear_object (&dialog->manager);
+  g_clear_object (&dialog->pixbuf_root);
+  g_clear_object (&dialog->pixbuf_missing);
+
+  G_OBJECT_CLASS (synced_tabs_dialog_parent_class)->dispose (object);
+}
+
+static void
+synced_tabs_dialog_class_init (SyncedTabsDialogClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->set_property = synced_tabs_dialog_set_property;
+  object_class->get_property = synced_tabs_dialog_get_property;
+  object_class->constructed = synced_tabs_dialog_constructed;
+  object_class->dispose = synced_tabs_dialog_dispose;
+
+  obj_properties[PROP_OPEN_TABS_MANAGER] =
+    g_param_spec_object ("open-tabs-manager",
+                         "Open tabs manager",
+                         "Open Tabs Manager",
+                         EPHY_TYPE_OPEN_TABS_MANAGER,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/epiphany/gtk/synced-tabs-dialog.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, SyncedTabsDialog, treestore);
+  gtk_widget_class_bind_template_child (widget_class, SyncedTabsDialog, treeview);
+  gtk_widget_class_bind_template_callback (widget_class, treeview_row_activated_cb);
+}
+
+static void
+synced_tabs_dialog_init (SyncedTabsDialog *dialog)
+{
+  WebKitWebContext *context;
+  GdkPixbuf *pixbuf;
+  GError *error = NULL;
+
+  gtk_widget_init_template (GTK_WIDGET (dialog));
+
+  gtk_tree_view_set_tooltip_column (GTK_TREE_VIEW (dialog->treeview), URL_COLUMN);
+
+  context = ephy_embed_shell_get_web_context (ephy_embed_shell_get_default ());
+  dialog->database = webkit_web_context_get_favicon_database (context);
+
+  dialog->pixbuf_root = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
+                                                  "computer-symbolic",
+                                                  FAVICON_SIZE, 0, &error);
+  if (error) {
+    g_warning ("Failed to build pixbuf from theme icon: %s", error->message);
+    g_error_free (error);
+    error = NULL;
+  }
+
+  pixbuf = gdk_pixbuf_new_from_resource (PIXBUF_MISSING_PATH, &error);
+  if (pixbuf) {
+    dialog->pixbuf_missing = gdk_pixbuf_scale_simple (pixbuf,
+                                                      FAVICON_SIZE, FAVICON_SIZE,
+                                                      GDK_INTERP_BILINEAR);
+    g_object_unref (pixbuf);
+  } else {
+    g_warning ("Failed to build pixbuf from resource: %s", error->message);
+    g_error_free (error);
+  }
+}
+
+SyncedTabsDialog *
+synced_tabs_dialog_new (EphyOpenTabsManager *manager)
+{
+  return EPHY_SYNCED_TABS_DIALOG (g_object_new (EPHY_TYPE_SYNCED_TABS_DIALOG,
+                                                "use-header-bar", TRUE,
+                                                "open-tabs-manager", manager,
+                                                NULL));
+}
diff --git a/src/synced-tabs-dialog.h b/src/synced-tabs-dialog.h
new file mode 100644
index 0000000..9081a09
--- /dev/null
+++ b/src/synced-tabs-dialog.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ephy-open-tabs-manager.h"
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_SYNCED_TABS_DIALOG (synced_tabs_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (SyncedTabsDialog, synced_tabs_dialog, EPHY, SYNCED_TABS_DIALOG, GtkDialog)
+
+SyncedTabsDialog *synced_tabs_dialog_new (EphyOpenTabsManager *manager);
+
+G_END_DECLS


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]