[gnome-calendar/gbsneto/timeline: 1/18] Introduce the new calendar monitoring API



commit b7b26a431e1b4bfad6180f93b65f2c50af61af33
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date:   Thu Mar 26 22:56:10 2020 -0300

    Introduce the new calendar monitoring API
    
    It is, as expected, heavily inspired by the ECalDataModel code,
    except it uses GDateTime, is timezone aware, and can handle all
    day events correctly.

 src/core/gcal-calendar-monitor.c    | 1131 +++++++++++++++++++++++++++++++++++
 src/core/gcal-calendar-monitor.h    |   41 ++
 src/core/gcal-event.c               |   21 +
 src/core/gcal-event.h               |    4 +
 src/core/gcal-timeline-subscriber.c |   96 +++
 src/core/gcal-timeline-subscriber.h |   59 ++
 src/core/gcal-timeline.c            |  713 ++++++++++++++++++++++
 src/core/gcal-timeline.h            |   46 ++
 src/gcal-types.h                    |    4 +
 src/meson.build                     |    3 +
 10 files changed, 2118 insertions(+)
---
diff --git a/src/core/gcal-calendar-monitor.c b/src/core/gcal-calendar-monitor.c
new file mode 100644
index 00000000..0bf66e2a
--- /dev/null
+++ b/src/core/gcal-calendar-monitor.c
@@ -0,0 +1,1131 @@
+/* gcal-calendar-monitor.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "GcalCalendarMonitor"
+
+#include "gcal-calendar-monitor.h"
+#include "gcal-date-time-utils.h"
+#include "gcal-debug.h"
+#include "gcal-event.h"
+
+#include <gio/gio.h>
+#include <libecal/libecal.h>
+
+typedef struct
+{
+  GcalCalendarMonitor *monitor;
+  GcalEvent           *event;
+  gchar               *event_id;
+} IdleData;
+
+typedef enum
+{
+  INVALID_EVENT,
+  CREATE_VIEW,
+  REMOVE_VIEW,
+  RANGE_UPDATED,
+  QUIT,
+} MonitorThreadEvent;
+
+struct _GcalCalendarMonitor
+{
+  GObject             parent;
+
+  GThread            *thread;
+  GCancellable       *cancellable;
+  GMainContext       *thread_context;
+  GMainContext       *main_context;
+
+  GHashTable         *events; /* gchar* -> GcalEvent* */
+
+  GAsyncQueue        *messages;
+  GcalCalendar       *calendar;
+
+  /*
+   * These fields are only accessed on the monitor thread, and
+   * never on the main thread.
+   */
+  struct {
+    gboolean          populated;
+    GHashTable       *events_to_add;
+    ECalClientView   *view;
+  } monitor_thread;
+
+  /*
+   * Accessing any of the fields below must be happen inside
+   * a locked muxed.
+   */
+  struct {
+    GMutex            mutex;
+    GDateTime        *range_start;
+    GDateTime        *range_end;
+  } shared;
+};
+
+static gboolean      add_event_to_timeline_in_idle_cb            (gpointer           user_data);
+static gboolean      update_event_in_idle_cb                     (gpointer           user_data);
+static gboolean      remove_event_from_timeline_in_idle_cb       (gpointer           user_data);
+
+G_DEFINE_TYPE (GcalCalendarMonitor, gcal_calendar_monitor, G_TYPE_OBJECT)
+
+enum
+{
+  EVENT_ADDED,
+  EVENT_UPDATED,
+  EVENT_REMOVED,
+  N_SIGNALS,
+};
+
+enum
+{
+  PROP_0,
+  PROP_CALENDAR,
+  N_PROPS
+};
+
+static gulong signals[N_SIGNALS] = { 0, };
+static GParamSpec *properties [N_PROPS] = { NULL, };
+
+/*
+ * Threads
+ *
+ * These methods must *never* be executed in the main thread.
+ */
+
+static gchar*
+build_subscriber_filter (GDateTime *range_start,
+                         GDateTime *range_end)
+{
+  g_autoptr (GDateTime) utc_range_start = NULL;
+  g_autoptr (GDateTime) utc_range_end = NULL;
+  g_autofree gchar *start_str = NULL;
+  g_autofree gchar *end_str = NULL;
+  g_autofree gchar *filter = NULL;
+
+  /*
+   * XXX: E-D-S ISO8601 parser is incomplete and doesn't accept the output of
+   * g_date_time_format_iso8601().
+   */
+
+  utc_range_start = g_date_time_to_utc (range_start);
+  start_str = g_date_time_format (utc_range_start, "%Y%m%dT%H%M%SZ");
+
+  utc_range_end = g_date_time_to_utc (range_end);
+  end_str = g_date_time_format (utc_range_end, "%Y%m%dT%H%M%SZ");
+
+  filter = g_strdup_printf ("(occur-in-time-range? (make-time \"%s\") (make-time \"%s\"))",
+                            start_str,
+                            end_str);
+
+  return g_steal_pointer (&filter);
+}
+
+static void
+maybe_init_event_arrays (GcalCalendarMonitor *self)
+{
+  if (self->monitor_thread.populated)
+    return;
+
+  if (!self->monitor_thread.events_to_add)
+    {
+      self->monitor_thread.events_to_add = g_hash_table_new_full (g_str_hash,
+                                                                  g_str_equal,
+                                                                  g_free,
+                                                                  g_object_unref);
+    }
+}
+
+static void
+get_monitor_ranges (GcalCalendarMonitor  *self,
+                    GDateTime           **out_range_start,
+                    GDateTime           **out_range_end)
+{
+  g_mutex_lock (&self->shared.mutex);
+
+  if (out_range_start)
+    *out_range_start = g_date_time_ref (self->shared.range_start);
+
+  if (out_range_end)
+    *out_range_end = g_date_time_ref (self->shared.range_end);
+
+  g_mutex_unlock (&self->shared.mutex);
+}
+
+static void
+idle_data_free (IdleData *data)
+{
+  g_clear_object (&data->monitor);
+  g_clear_object (&data->event);
+  g_clear_pointer (&data->event_id, g_free);
+  g_free (data);
+}
+
+static void
+add_event_in_idle (GcalCalendarMonitor *self,
+                   GcalEvent        *event)
+{
+  IdleData *idle_data;
+
+  idle_data = g_new0 (IdleData, 1);
+  idle_data->monitor = g_object_ref (self);
+  idle_data->event = g_object_ref (event);
+
+  g_main_context_invoke_full (self->main_context,
+                              G_PRIORITY_DEFAULT_IDLE,
+                              add_event_to_timeline_in_idle_cb,
+                              idle_data,
+                              (GDestroyNotify) idle_data_free);
+}
+
+static void
+update_event_in_idle (GcalCalendarMonitor *self,
+                      GcalEvent           *event)
+{
+  IdleData *idle_data;
+
+  idle_data = g_new0 (IdleData, 1);
+  idle_data->monitor = g_object_ref (self);
+  idle_data->event = g_object_ref (event);
+
+  g_main_context_invoke_full (self->main_context,
+                              G_PRIORITY_DEFAULT_IDLE,
+                              update_event_in_idle_cb,
+                              idle_data,
+                              (GDestroyNotify) idle_data_free);
+}
+
+static void
+remove_event_in_idle (GcalCalendarMonitor *self,
+                      const gchar         *event_id)
+{
+  IdleData *idle_data;
+
+  idle_data = g_new0 (IdleData, 1);
+  idle_data->monitor = g_object_ref (self);
+  idle_data->event_id = g_strdup (event_id);
+
+  g_main_context_invoke_full (self->main_context,
+                              G_PRIORITY_DEFAULT_IDLE,
+                              remove_event_from_timeline_in_idle_cb,
+                              idle_data,
+                              (GDestroyNotify) idle_data_free);
+}
+
+typedef struct
+{
+  GcalCalendarMonitor *monitor;
+  GPtrArray           *expanded_events;
+} GenerateRecurrencesData;
+
+static gboolean
+client_instance_generated_cb (ICalComponent  *icomponent,
+                              ICalTime       *instance_start,
+                              ICalTime       *instance_end,
+                              gpointer        user_data,
+                              GCancellable   *cancellable,
+                              GError        **error)
+{
+  g_autoptr (GcalEvent) event = NULL;
+  g_autoptr (GError) local_error = NULL;
+  GenerateRecurrencesData *data;
+  GcalCalendarMonitor *self;
+  ECalComponent *ecomponent;
+
+  data = user_data;
+  self = data->monitor;
+
+  if (g_cancellable_set_error_if_cancelled (cancellable, error))
+    return FALSE;
+
+  ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent));
+  if (!ecomponent)
+    return TRUE;
+
+  event = gcal_event_new (self->calendar, ecomponent, &local_error);
+  if (local_error)
+    {
+      g_propagate_error (error, local_error);
+      return TRUE;
+    }
+
+  g_ptr_array_add (data->expanded_events, g_steal_pointer (&event));
+
+  return TRUE;
+}
+
+static void
+on_client_view_objects_added_cb (ECalClientView      *view,
+                                 const GSList        *objects,
+                                 GcalCalendarMonitor *self)
+{
+  g_autoptr (GPtrArray) components_to_expand = NULL;
+  g_autoptr (GDateTime) range_start = NULL;
+  g_autoptr (GDateTime) range_end = NULL;
+  const GSList *l;
+  gint i;
+
+  GCAL_ENTRY;
+
+  get_monitor_ranges (self, &range_start, &range_end);
+
+  maybe_init_event_arrays (self);
+  components_to_expand = g_ptr_array_new ();
+
+  for (l = objects; l; l = l->next)
+    {
+      g_autoptr (GcalEvent) event = NULL;
+      g_autoptr (GError) error = NULL;
+      g_autofree gchar *event_id = NULL;
+      ICalComponent *icomponent;
+      ECalComponent *ecomponent;
+
+      icomponent = l->data;
+
+      if (g_cancellable_is_cancelled (self->cancellable))
+        return;
+
+      if (!icomponent || !i_cal_component_get_uid (icomponent))
+        continue;
+
+      /* Recurrent events will be processed later */
+      if (e_cal_util_component_has_recurrences (icomponent) &&
+          !e_cal_util_component_is_instance (icomponent))
+        {
+          GCAL_TRACE_MSG ("Component %s (%s) needs to be expanded",
+                          i_cal_component_get_summary (icomponent),
+                          i_cal_component_get_uid (icomponent));
+          g_ptr_array_add (components_to_expand, icomponent);
+          continue;
+        }
+
+      ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent));
+
+      if (!ecomponent)
+        continue;
+
+      event = gcal_event_new (self->calendar, ecomponent, &error);
+
+      if (error)
+        {
+          g_warning ("Error creating event: %s", error->message);
+          continue;
+        }
+
+      if (!gcal_event_is_within_range (event, range_start, range_end))
+        continue;
+
+      event_id = g_strdup (gcal_event_get_uid (event));
+
+      if (!self->monitor_thread.populated)
+        {
+          g_hash_table_insert (self->monitor_thread.events_to_add,
+                               g_steal_pointer (&event_id),
+                               g_object_ref (event));
+        }
+      else
+        {
+          add_event_in_idle (self, event);
+        }
+    }
+
+  /* Recurrent events */
+  if (components_to_expand->len > 0)
+    {
+      g_autoptr (GPtrArray) expanded_events = NULL;
+      ECalClient *client;
+      time_t range_start_time;
+      time_t range_end_time;
+
+      GCAL_TRACE_MSG ("Expanding recurrencies of %d events", components_to_expand->len);
+
+      client = gcal_calendar_get_client (self->calendar);
+
+      range_start_time = g_date_time_to_unix (range_start);
+      range_end_time = g_date_time_to_unix (range_end);
+
+      expanded_events = g_ptr_array_new_with_free_func (g_object_unref);
+
+      /* Generate the instances */
+      for (i = 0; i < components_to_expand->len; i++)
+        {
+          GenerateRecurrencesData recurrences_data;
+          ICalComponent *icomponent;
+          gint old_size;
+
+          if (g_cancellable_is_cancelled (self->cancellable))
+            return;
+
+          icomponent = g_ptr_array_index (components_to_expand, i);
+
+          recurrences_data.monitor = self;
+          recurrences_data.expanded_events = expanded_events;
+
+          old_size = expanded_events->len;
+          e_cal_client_generate_instances_for_object_sync (client,
+                                                           icomponent,
+                                                           range_start_time,
+                                                           range_end_time,
+                                                           self->cancellable,
+                                                           client_instance_generated_cb,
+                                                           &recurrences_data);
+
+#if GCAL_ENABLE_TRACE
+            {
+              g_autofree gchar *range_start_str = g_date_time_format_iso8601 (range_start);
+              g_autofree gchar *range_end_str = g_date_time_format_iso8601 (range_end);
+
+              GCAL_TRACE_MSG ("Component %s (%s) added %d instance(s) between %s and %s",
+                              i_cal_component_get_summary (icomponent),
+                              i_cal_component_get_uid (icomponent),
+                              expanded_events->len - old_size,
+                              range_start_str,
+                              range_end_str);
+            }
+#endif
+        }
+
+      /* Process all these instances */
+      for (i = 0; i < expanded_events->len; i++)
+        {
+          g_autofree gchar *event_id = NULL;
+          GcalEvent *event = g_ptr_array_index (expanded_events, i);
+
+          if (g_cancellable_is_cancelled (self->cancellable))
+            return;
+
+          if (!gcal_event_is_within_range (event, range_start, range_end))
+            continue;
+
+          event_id = g_strdup (gcal_event_get_uid (event));
+
+          if (!self->monitor_thread.populated)
+            {
+              g_hash_table_insert (self->monitor_thread.events_to_add,
+                                   g_steal_pointer (&event_id),
+                                   g_object_ref (event));
+            }
+          else
+            {
+              add_event_in_idle (self, event);
+            }
+        }
+    }
+
+  GCAL_EXIT;
+}
+
+static void
+on_client_view_objects_modified_cb (ECalClientView      *view,
+                                    const GSList        *objects,
+                                    GcalCalendarMonitor *self)
+{
+  g_autoptr (GDateTime) range_start = NULL;
+  g_autoptr (GDateTime) range_end = NULL;
+  const GSList *l;
+
+  GCAL_ENTRY;
+
+  if (!self->monitor_thread.populated && self->monitor_thread.events_to_add)
+    {
+      g_clear_pointer (&self->monitor_thread.events_to_add, g_hash_table_destroy);
+      return;
+    }
+
+  get_monitor_ranges (self, &range_start, &range_end);
+
+  for (l = objects; l; l = l->next)
+    {
+      g_autoptr (GcalEvent) event = NULL;
+      g_autoptr (GError) error = NULL;
+      ICalComponent *icomponent;
+      ECalComponent *ecomponent;
+
+      icomponent = l->data;
+
+      if (!icomponent || !i_cal_component_get_uid (icomponent))
+        continue;
+
+      ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent));
+
+      if (!ecomponent)
+        continue;
+
+      event = gcal_event_new (self->calendar, ecomponent, &error);
+
+      if (error)
+        {
+          g_warning ("Error creating event: %s", error->message);
+          continue;
+        }
+
+      update_event_in_idle (self, event);
+    }
+
+  GCAL_EXIT;
+}
+
+static void
+on_client_view_objects_removed_cb (ECalClientView      *view,
+                                   const GSList        *objects,
+                                   GcalCalendarMonitor *self)
+{
+  const GSList *l;
+
+  GCAL_ENTRY;
+
+  for (l = objects; l; l = l->next)
+    {
+      g_autofree gchar *event_id = NULL;
+      ECalComponentId *component_id;
+
+      if (g_cancellable_is_cancelled (self->cancellable))
+        return;
+
+      component_id = l->data;
+
+      if (e_cal_component_id_get_rid (component_id))
+        {
+          event_id = g_strdup_printf ("%s:%s:%s",
+                                      gcal_calendar_get_id (self->calendar),
+                                      e_cal_component_id_get_uid (component_id),
+                                      e_cal_component_id_get_rid (component_id));
+        }
+      else
+        {
+          event_id = g_strdup_printf ("%s:%s",
+                                      gcal_calendar_get_id (self->calendar),
+                                      e_cal_component_id_get_uid (component_id));
+        }
+
+      remove_event_in_idle (self, event_id);
+    }
+
+  GCAL_EXIT;
+}
+
+static void
+on_client_view_complete_cb (ECalClientView      *view,
+                            const GError        *error,
+                            GcalCalendarMonitor *self)
+{
+  g_autoptr (GHashTable) events_to_add = NULL;
+
+  GCAL_ENTRY;
+
+  g_assert (!self->monitor_thread.populated);
+
+  events_to_add = g_steal_pointer (&self->monitor_thread.events_to_add);
+
+  /* Process all these instances */
+  if (events_to_add)
+    {
+      GHashTableIter iter;
+      GcalEvent *event;
+
+      g_hash_table_iter_init (&iter, events_to_add);
+      while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &event))
+        {
+          if (g_cancellable_is_cancelled (self->cancellable))
+            return;
+
+          GCAL_TRACE_MSG ("Sending event '%s' (%s) to main thead's idle",
+                          gcal_event_get_summary (event),
+                          gcal_event_get_uid (event));
+
+          add_event_in_idle (self, event);
+        }
+    }
+
+  self->monitor_thread.populated = TRUE;
+
+  g_debug ("Finished initial loading of calendar '%s'", gcal_calendar_get_name (self->calendar));
+
+  GCAL_EXIT;
+}
+
+static void
+create_view (GcalCalendarMonitor *self)
+{
+  g_autofree gchar *filter = NULL;
+  g_autoptr (GError) error = NULL;
+  ECalClientView *view;
+  ECalClient *client;
+
+  GCAL_ENTRY;
+
+  g_mutex_lock (&self->shared.mutex);
+
+  g_assert (self->cancellable == NULL);
+  self->cancellable = g_cancellable_new ();
+
+  g_assert (self->shared.range_start != NULL);
+  g_assert (self->shared.range_end != NULL);
+
+  filter = build_subscriber_filter (self->shared.range_start, self->shared.range_end);
+
+  g_mutex_unlock (&self->shared.mutex);
+
+  if (!gcal_calendar_get_visible (self->calendar))
+    GCAL_RETURN ();
+
+  client = gcal_calendar_get_client (self->calendar);
+  e_cal_client_get_view_sync (client, filter, &view, self->cancellable, &error);
+
+  if (error)
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        g_warning ("Error creating view: %s", error->message);
+      GCAL_RETURN ();
+    }
+
+  GCAL_TRACE_MSG ("Initialized ECalClientView with query \"%s\"", filter);
+
+  g_signal_connect (view, "objects-added", G_CALLBACK (on_client_view_objects_added_cb), self);
+  g_signal_connect (view, "objects-modified", G_CALLBACK (on_client_view_objects_modified_cb), self);
+  g_signal_connect (view, "objects-removed", G_CALLBACK (on_client_view_objects_removed_cb), self);
+  g_signal_connect (view, "complete", G_CALLBACK (on_client_view_complete_cb), self);
+
+  if (g_cancellable_is_cancelled (self->cancellable))
+    GCAL_RETURN ();
+
+  g_debug ("Starting ECalClientView for calendar '%s'", gcal_calendar_get_name (self->calendar));
+
+  e_cal_client_view_start (view, &error);
+
+  if (error)
+    {
+      g_warning ("Error starting up view: %s", error->message);
+      g_clear_object (&view);
+      GCAL_RETURN ();
+    }
+
+  self->monitor_thread.view = g_steal_pointer (&view);
+
+  GCAL_EXIT;
+}
+
+static void
+remove_view (GcalCalendarMonitor *self)
+{
+  g_autoptr (GError) error = NULL;
+
+  GCAL_ENTRY;
+
+  g_clear_object (&self->cancellable);
+
+  if (!self->monitor_thread.view)
+    GCAL_RETURN ();
+
+  g_debug ("Tearing down view for calendar %s", gcal_calendar_get_id (self->calendar));
+
+  e_cal_client_view_stop (self->monitor_thread.view, &error);
+
+  if (error)
+    {
+      g_warning ("Error stopping view: %s", error->message);
+      GCAL_RETURN ();
+    }
+
+  g_clear_object (&self->monitor_thread.view);
+  self->monitor_thread.populated = FALSE;
+
+  GCAL_EXIT;
+}
+
+typedef struct
+{
+  GSource              parent;
+  GcalCalendarMonitor *monitor;
+  GMainLoop           *mainloop;
+} MessageQueueSource;
+
+static gboolean
+message_queue_source_prepare (GSource *source,
+                              gint    *timeout)
+{
+  GcalCalendarMonitor *self;
+  MessageQueueSource *queue_source;
+
+  queue_source = (MessageQueueSource*) source;
+  self = queue_source->monitor;
+
+  return g_async_queue_length (self->messages) > 0;
+}
+
+static gboolean
+message_queue_source_dispatch (GSource     *source,
+                               GSourceFunc  callback,
+                               gpointer     user_data)
+{
+  GcalCalendarMonitor *self;
+  MessageQueueSource *queue_source;
+  MonitorThreadEvent event;
+
+  GCAL_ENTRY;
+
+  queue_source = (MessageQueueSource*) source;
+  self = queue_source->monitor;
+  event = GPOINTER_TO_INT (g_async_queue_pop (self->messages));
+
+  switch (event)
+    {
+    case RANGE_UPDATED:
+      remove_view (self);
+      create_view (self);
+      break;
+
+    case CREATE_VIEW:
+      create_view (self);
+      break;
+
+    case REMOVE_VIEW:
+      remove_view (self);
+      break;
+
+    case INVALID_EVENT:
+    case QUIT:
+      remove_view (self);
+      g_main_loop_quit (queue_source->mainloop);
+      GCAL_RETURN (G_SOURCE_REMOVE);
+    }
+
+  GCAL_RETURN (G_SOURCE_CONTINUE);
+}
+
+static GSourceFuncs monitor_queue_source_funcs =
+{
+  message_queue_source_prepare,
+  NULL,
+  message_queue_source_dispatch,
+  NULL,
+};
+
+static MessageQueueSource*
+views_monitor_source_new (GcalCalendarMonitor *self,
+                          GMainLoop           *mainloop)
+{
+  MessageQueueSource *queue_source;
+  GSource *source;
+
+  /* Prepare the message queue source */
+  source = g_source_new (&monitor_queue_source_funcs, sizeof (MessageQueueSource));
+  queue_source = (MessageQueueSource*) source;
+  queue_source->monitor = self;
+  queue_source->mainloop = mainloop;
+  g_source_set_name (source, "Message Queue Source");
+
+  return queue_source;
+}
+
+static gpointer
+calendar_view_thread_func (gpointer data)
+{
+  g_autoptr (GMainLoop) mainloop = NULL;
+  MessageQueueSource *queue_source;
+  GcalCalendarMonitor *self;
+
+  GCAL_ENTRY;
+
+  self = data;
+  mainloop = g_main_loop_new (self->thread_context, FALSE);
+  g_main_context_push_thread_default (self->thread_context);
+
+  /* Events source */
+  queue_source = views_monitor_source_new (self, mainloop);
+  g_source_attach ((GSource*) queue_source, self->thread_context);
+
+  g_main_loop_run (mainloop);
+  g_main_loop_quit (mainloop);
+
+  g_main_context_pop_thread_default (self->thread_context);
+
+  GCAL_RETURN (NULL);
+}
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+notify_view_thread (GcalCalendarMonitor *self,
+                    MonitorThreadEvent   event)
+{
+  g_async_queue_push (self->messages, GINT_TO_POINTER (event));
+  g_main_context_wakeup (self->thread_context);
+}
+
+static void
+maybe_spawn_view_thread (GcalCalendarMonitor *self)
+{
+  g_autofree gchar *thread_name = NULL;
+
+  if (self->thread || !self->shared.range_start || !self->shared.range_end)
+    return;
+
+  thread_name = g_strdup_printf ("GcalCalendarMonitor (%s)", gcal_calendar_get_id (self->calendar));
+  self->thread = g_thread_new (thread_name, calendar_view_thread_func, self);
+
+  g_debug ("Spawning thread %s", thread_name);
+}
+
+static void
+remove_events_outside_range (GcalCalendarMonitor *self,
+                             GDateTime           *range_start,
+                             GDateTime           *range_end)
+{
+  GHashTableIter iter;
+  GcalEvent *event;
+
+  GCAL_TRACE_MSG ("Removing events outside range from monitor");
+
+  g_hash_table_iter_init (&iter, self->events);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &event))
+    {
+      if (gcal_event_is_within_range (event, range_start, range_end))
+        continue;
+
+      g_object_ref (event);
+
+      g_hash_table_iter_remove (&iter);
+      g_signal_emit (self, signals[EVENT_REMOVED], 0, event);
+
+      g_object_unref (event);
+    }
+}
+
+static void
+remove_all_events (GcalCalendarMonitor *self)
+{
+  GHashTableIter iter;
+  GcalEvent *event;
+
+  GCAL_TRACE_MSG ("Removing all events from view");
+
+  g_hash_table_iter_init (&iter, self->events);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &event))
+    {
+      g_hash_table_iter_remove (&iter);
+      g_signal_emit (self, signals[EVENT_REMOVED], 0, event);
+    }
+}
+
+
+/*
+ * Callbacks
+ */
+
+static gboolean
+add_event_to_timeline_in_idle_cb (gpointer user_data)
+{
+  GcalCalendarMonitor *self;
+  GcalEvent *event;
+  IdleData *idle_data;
+  const gchar *event_id;
+
+  GCAL_ENTRY;
+
+  idle_data = user_data;
+  self = idle_data->monitor;
+  event = idle_data->event;
+  g_assert (idle_data->event_id == NULL);
+
+  event_id = gcal_event_get_uid (event);
+  if (g_hash_table_insert (self->events, g_strdup (event_id), g_object_ref (event)))
+    g_signal_emit (self, signals[EVENT_ADDED], 0, event);
+
+  GCAL_RETURN (G_SOURCE_REMOVE);
+}
+
+static gboolean
+update_event_in_idle_cb (gpointer user_data)
+{
+  GcalCalendarMonitor *self;
+  GcalEvent *old_event;
+  GcalEvent *event;
+  IdleData *idle_data;
+  const gchar *event_id;
+
+  GCAL_ENTRY;
+
+  idle_data = user_data;
+  self = idle_data->monitor;
+  event = idle_data->event;
+  g_assert (idle_data->event_id == NULL);
+
+  event_id = gcal_event_get_uid (event);
+  old_event = g_hash_table_lookup (self->events, event_id);
+  if (old_event)
+    {
+      g_hash_table_insert (self->events, g_strdup (event_id), g_object_ref (event));
+      g_signal_emit (self, signals[EVENT_UPDATED], 0, old_event, event);
+    }
+
+  GCAL_RETURN (G_SOURCE_REMOVE);
+}
+
+static gboolean
+remove_event_from_timeline_in_idle_cb (gpointer user_data)
+{
+  g_autoptr (GcalEvent) event = NULL;
+  GcalCalendarMonitor *self;
+  IdleData *idle_data;
+  gchar *event_id;
+
+  GCAL_ENTRY;
+
+  idle_data = user_data;
+  self = idle_data->monitor;
+  g_assert (idle_data->event == NULL);
+  event_id = idle_data->event_id;
+
+  event = g_hash_table_lookup (self->events, event_id);
+
+  if (event)
+    {
+      /* Keep the event alive until the signal is emitted */
+      g_object_ref (event);
+
+      g_hash_table_remove (self->events, event_id);
+      g_signal_emit (self, signals[EVENT_REMOVED], 0, event);
+    }
+
+  GCAL_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+on_calendar_visible_changed_cb (GcalCalendar        *calendar,
+                                GParamSpec          *pspec,
+                                GcalCalendarMonitor *self)
+{
+  if (gcal_calendar_get_visible (calendar))
+    {
+      notify_view_thread (self, CREATE_VIEW);
+    }
+  else
+    {
+      remove_all_events (self);
+      notify_view_thread (self, REMOVE_VIEW);
+    }
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gcal_calendar_monitor_finalize (GObject *object)
+{
+  GcalCalendarMonitor *self = (GcalCalendarMonitor *)object;
+
+  g_cancellable_cancel (self->cancellable);
+  notify_view_thread (self, QUIT);
+  g_thread_join (self->thread);
+
+  remove_all_events (self);
+
+  g_clear_object (&self->calendar);
+  g_clear_pointer (&self->events, g_hash_table_destroy);
+  g_clear_pointer (&self->thread_context, g_main_context_unref);
+  g_clear_pointer (&self->main_context, g_main_context_unref);
+  g_clear_pointer (&self->messages, g_async_queue_unref);
+  gcal_clear_date_time (&self->shared.range_start);
+  gcal_clear_date_time (&self->shared.range_end);
+
+  G_OBJECT_CLASS (gcal_calendar_monitor_parent_class)->finalize (object);
+}
+
+static void
+gcal_calendar_monitor_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  GcalCalendarMonitor *self = GCAL_CALENDAR_MONITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_CALENDAR:
+      g_value_set_object (value, self->calendar);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_calendar_monitor_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  GcalCalendarMonitor *self = GCAL_CALENDAR_MONITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_CALENDAR:
+      g_assert (self->calendar == NULL);
+      self->calendar = g_value_dup_object (value);
+      g_signal_connect (self->calendar,
+                        "notify::visible",
+                        G_CALLBACK (on_calendar_visible_changed_cb),
+                        self);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_calendar_monitor_class_init (GcalCalendarMonitorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gcal_calendar_monitor_finalize;
+  object_class->get_property = gcal_calendar_monitor_get_property;
+  object_class->set_property = gcal_calendar_monitor_set_property;
+
+  signals[EVENT_ADDED] = g_signal_new ("event-added",
+                                       GCAL_TYPE_CALENDAR_MONITOR,
+                                       G_SIGNAL_RUN_FIRST,
+                                       0, NULL, NULL, NULL,
+                                       G_TYPE_NONE,
+                                       1,
+                                       GCAL_TYPE_EVENT);
+
+  signals[EVENT_UPDATED] = g_signal_new ("event-updated",
+                                         GCAL_TYPE_CALENDAR_MONITOR,
+                                         G_SIGNAL_RUN_FIRST,
+                                         0, NULL, NULL, NULL,
+                                         G_TYPE_NONE,
+                                         2,
+                                         GCAL_TYPE_EVENT,
+                                         GCAL_TYPE_EVENT);
+
+  signals[EVENT_REMOVED] = g_signal_new ("event-removed",
+                                         GCAL_TYPE_CALENDAR_MONITOR,
+                                         G_SIGNAL_RUN_FIRST,
+                                         0, NULL, NULL, NULL,
+                                         G_TYPE_NONE,
+                                         1,
+                                         GCAL_TYPE_EVENT);
+
+  properties[PROP_CALENDAR] = g_param_spec_object ("calendar",
+                                                   "Calendar",
+                                                   "Calendar to be monitores",
+                                                   GCAL_TYPE_CALENDAR,
+                                                   G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gcal_calendar_monitor_init (GcalCalendarMonitor *self)
+{
+  self->events = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+  self->thread_context = g_main_context_new ();
+  self->main_context = g_main_context_ref_thread_default ();
+  self->messages = g_async_queue_new ();
+}
+
+GcalCalendarMonitor*
+gcal_calendar_monitor_new (GcalCalendar *calendar)
+{
+  return g_object_new (GCAL_TYPE_CALENDAR_MONITOR,
+                       "calendar", calendar,
+                       NULL);
+}
+
+/**
+ * gcal_calendar_monitor_set_range:
+ * @self: a #GcalCalendarMonitor
+ * @range_start: new range start
+ * @range_end: new range end
+ *
+ * Updates the range of @self. This usually will result in
+ * @self creating a new view, and gathering the events from
+ * the events server.
+ */
+void
+gcal_calendar_monitor_set_range (GcalCalendarMonitor *self,
+                                 GDateTime           *range_start,
+                                 GDateTime           *range_end)
+{
+  gboolean range_changed;
+
+  g_return_if_fail (GCAL_IS_CALENDAR_MONITOR (self));
+  g_return_if_fail (range_start != NULL);
+  g_return_if_fail (range_end != NULL);
+  g_return_if_fail (g_date_time_compare (range_start, range_end) < 0);
+
+  GCAL_ENTRY;
+
+  g_mutex_lock (&self->shared.mutex);
+
+  range_changed =
+    !self->shared.range_start ||
+    !self->shared.range_end ||
+    (self->shared.range_start && range_start && g_date_time_compare (self->shared.range_start, range_start) 
!= 0) ||
+    (self->shared.range_end && range_end && g_date_time_compare (self->shared.range_end, range_end) != 0);
+
+  if (!range_changed)
+    {
+      g_mutex_unlock (&self->shared.mutex);
+      GCAL_RETURN ();
+    }
+
+  gcal_set_date_time (&self->shared.range_start, range_start);
+  gcal_set_date_time (&self->shared.range_end, range_end);
+
+  g_mutex_unlock (&self->shared.mutex);
+
+  maybe_spawn_view_thread (self);
+  remove_events_outside_range (self, range_start, range_end);
+
+  g_cancellable_cancel (self->cancellable);
+
+  if (gcal_calendar_get_visible (self->calendar))
+    notify_view_thread (self, RANGE_UPDATED);
+
+  GCAL_EXIT;
+}
+
+/**
+ * gcal_calendar_monitor_get_cached_event:
+ * @self: a #GcalCalendarMonitor
+ * @event_id: the id of the #GcalEvent
+ *
+ * Returns: (transfer full)(nullable): a #GcalEvent, or %NULL
+ */
+GcalEvent*
+gcal_calendar_monitor_get_cached_event (GcalCalendarMonitor *self,
+                                        const gchar         *event_id)
+{
+  GcalEvent *event;
+
+  g_return_val_if_fail (GCAL_IS_CALENDAR_MONITOR (self), NULL);
+  g_return_val_if_fail (event_id, NULL);
+
+  event = g_hash_table_lookup (self->events, event_id);
+
+  return event ? g_object_ref (event) : NULL;
+}
diff --git a/src/core/gcal-calendar-monitor.h b/src/core/gcal-calendar-monitor.h
new file mode 100644
index 00000000..a6423f16
--- /dev/null
+++ b/src/core/gcal-calendar-monitor.h
@@ -0,0 +1,41 @@
+/* gcal-calendar-monitor.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gcal-types.h"
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_CALENDAR_MONITOR (gcal_calendar_monitor_get_type())
+G_DECLARE_FINAL_TYPE (GcalCalendarMonitor, gcal_calendar_monitor, GCAL, CALENDAR_MONITOR, GObject)
+
+GcalCalendarMonitor* gcal_calendar_monitor_new                   (GcalCalendar        *calendar);
+
+void                 gcal_calendar_monitor_set_range             (GcalCalendarMonitor *self,
+                                                                  GDateTime           *range_start,
+                                                                  GDateTime           *range_end);
+
+GcalEvent*           gcal_calendar_monitor_get_cached_event     (GcalCalendarMonitor  *self,
+                                                                 const gchar          *event_id);
+
+G_END_DECLS
diff --git a/src/core/gcal-event.c b/src/core/gcal-event.c
index 831ea644..98047c50 100644
--- a/src/core/gcal-event.c
+++ b/src/core/gcal-event.c
@@ -1829,3 +1829,24 @@ gcal_event_format_date (GcalEvent *self)
 
   return g_steal_pointer (&formatted_string);
 }
+
+gboolean
+gcal_event_is_within_range (GcalEvent *self,
+                            GDateTime *range_start,
+                            GDateTime *range_end)
+{
+  GDateTime *event_start;
+  GDateTime *event_end;
+
+  event_start = gcal_event_get_date_start (self);
+  event_end = gcal_event_get_date_end (self);
+
+  if (gcal_event_get_all_day (self))
+    {
+      return gcal_date_time_compare_date (range_end, event_start) < 0 &&
+             gcal_date_time_compare_date (range_start, event_end) > 0;
+    }
+
+  return g_date_time_compare (range_end, event_start) > 0 &&
+         g_date_time_compare (event_end, range_start) > 0;
+}
diff --git a/src/core/gcal-event.h b/src/core/gcal-event.h
index 7ec8a4af..be6e6c0c 100644
--- a/src/core/gcal-event.h
+++ b/src/core/gcal-event.h
@@ -131,6 +131,10 @@ void                 gcal_event_get_original_timezones           (GcalEvent
 
 gchar*               gcal_event_format_date                      (GcalEvent          *self);
 
+gboolean             gcal_event_is_within_range                  (GcalEvent          *self,
+                                                                  GDateTime          *range_start,
+                                                                  GDateTime          *range_end);
+
 G_END_DECLS
 
 #endif /* GCAL_EVENT_H */
diff --git a/src/core/gcal-timeline-subscriber.c b/src/core/gcal-timeline-subscriber.c
new file mode 100644
index 00000000..5a907ab4
--- /dev/null
+++ b/src/core/gcal-timeline-subscriber.c
@@ -0,0 +1,96 @@
+/* gcal-timeline-subscriber.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "gcal-timeline-subscriber.h"
+
+G_DEFINE_INTERFACE (GcalTimelineSubscriber, gcal_timeline_subscriber, G_TYPE_OBJECT)
+
+enum
+{
+  RANGE_CHANGED,
+  N_SIGNALS,
+};
+
+static guint signals[N_SIGNALS] = { 0, };
+
+static void
+gcal_timeline_subscriber_default_init (GcalTimelineSubscriberInterface *iface)
+{
+  /**
+   * GcalTimelineSubscriber::range-changed:
+   *
+   * Emitted when the subscriber range is changed.
+   */
+  signals[RANGE_CHANGED] = g_signal_new ("range-changed",
+                                         GCAL_TYPE_TIMELINE_SUBSCRIBER,
+                                         G_SIGNAL_RUN_LAST,
+                                         0,
+                                         NULL, NULL, NULL,
+                                         G_TYPE_NONE,
+                                         0);
+}
+
+/**
+ * gcal_timeline_subscriber_get_range_start:
+ * @self: a #GcalTimelineSubscriber
+ *
+ * Retrieves the start of the range of @self.
+ *
+ * Returns: (transfer full): a #GDateTime
+ */
+GDateTime*
+gcal_timeline_subscriber_get_range_start (GcalTimelineSubscriber *self)
+{
+  g_return_val_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (self), NULL);
+
+  return GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (self)->get_range_start (self);
+}
+
+/**
+ * gcal_timeline_subscriber_get_range_end:
+ * @self: a #GcalTimelineSubscriber
+ *
+ * Retrieves the end of the range of @self.
+ *
+ * Returns: (transfer full): a #GDateTime
+ */
+GDateTime*
+gcal_timeline_subscriber_get_range_end (GcalTimelineSubscriber *self)
+{
+  g_return_val_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (self), NULL);
+
+  return GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (self)->get_range_end (self);
+}
+
+/**
+ * gcal_timeline_subscriber_range_changed:
+ * @self: a #GcalTimelineSubscriber
+ *
+ * Emits the "range-changed" signal for @self. Only
+ * implementations of #GcalTimelineSubscriber are
+ * expected to use this method.
+ */
+void
+gcal_timeline_subscriber_range_changed (GcalTimelineSubscriber *self)
+{
+  g_return_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (self));
+
+  g_signal_emit (self, signals[RANGE_CHANGED], 0);
+}
diff --git a/src/core/gcal-timeline-subscriber.h b/src/core/gcal-timeline-subscriber.h
new file mode 100644
index 00000000..f916953e
--- /dev/null
+++ b/src/core/gcal-timeline-subscriber.h
@@ -0,0 +1,59 @@
+/* gcal-timeline-subscriber.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#define LIBICAL_GLIB_UNSTABLE_API
+#include <libical-glib/libical-glib.h>
+
+#include "gcal-types.h"
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_TIMELINE_SUBSCRIBER (gcal_timeline_subscriber_get_type ())
+
+G_DECLARE_INTERFACE (GcalTimelineSubscriber, gcal_timeline_subscriber, GCAL, TIMELINE_SUBSCRIBER, GObject)
+
+struct _GcalTimelineSubscriberInterface
+{
+  GTypeInterface parent;
+
+  GDateTime*         (*get_range_start)                          (GcalTimelineSubscriber *self);
+  GDateTime*         (*get_range_end)                            (GcalTimelineSubscriber *self);
+
+  void               (*add_event)                                (GcalTimelineSubscriber *self,
+                                                                  GcalEvent              *event);
+
+  void               (*update_event)                             (GcalTimelineSubscriber *self,
+                                                                  GcalEvent              *event);
+
+  void               (*remove_event)                             (GcalTimelineSubscriber *self,
+                                                                  GcalEvent              *event);
+};
+
+GDateTime*           gcal_timeline_subscriber_get_range_start      (GcalTimelineSubscriber  *self);
+
+GDateTime*           gcal_timeline_subscriber_get_range_end        (GcalTimelineSubscriber  *self);
+
+void                 gcal_timeline_subscriber_range_changed        (GcalTimelineSubscriber *self);
+
+G_END_DECLS
diff --git a/src/core/gcal-timeline.c b/src/core/gcal-timeline.c
new file mode 100644
index 00000000..4ad57102
--- /dev/null
+++ b/src/core/gcal-timeline.c
@@ -0,0 +1,713 @@
+/* gcal-timeline.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "GcalTimeline"
+
+#include "gcal-calendar.h"
+#include "gcal-calendar-monitor.h"
+#include "gcal-context.h"
+#include "gcal-date-time-utils.h"
+#include "gcal-debug.h"
+#include "gcal-event.h"
+#include "gcal-range-tree.h"
+#include "gcal-timeline.h"
+#include "gcal-timeline-subscriber.h"
+
+#include <libedataserver/libedataserver.h>
+
+typedef struct
+{
+  GDateTime          *range_start;
+  GDateTime          *range_end;
+} SubscriberData;
+
+struct _GcalTimeline
+{
+  GObject             parent_instance;
+
+  gulong              update_range_idle_id;
+  GDateTime          *range_start;
+  GDateTime          *range_end;
+
+  GcalRangeTree      *events;
+
+  GHashTable         *calendars; /* GcalCalendar* -> GcalCalendarMonitor* */
+  GHashTable         *subscribers; /* GcalTimelineSubscriber* -> SubscriberData* */
+  GcalRangeTree      *subscriber_ranges;
+
+  GCancellable       *cancellable;
+
+  GcalContext        *context;
+};
+
+G_DEFINE_TYPE (GcalTimeline, gcal_timeline, G_TYPE_OBJECT)
+
+enum
+{
+  PROP_0,
+  PROP_CONTEXT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS] = { NULL, };
+
+
+/*
+ * Auxiliary methods
+ */
+
+static SubscriberData*
+subscriber_data_new (GcalTimelineSubscriber *subscriber)
+{
+  SubscriberData *subscriber_data;
+
+  subscriber_data = g_new0 (SubscriberData, 1);
+  subscriber_data->range_start = gcal_timeline_subscriber_get_range_start (subscriber);
+  subscriber_data->range_end = gcal_timeline_subscriber_get_range_end (subscriber);
+
+  return g_steal_pointer (&subscriber_data);
+}
+
+static void
+subscriber_data_free (SubscriberData *subscriber_data)
+{
+  gcal_clear_date_time (&subscriber_data->range_start);
+  gcal_clear_date_time (&subscriber_data->range_end);
+  g_free (subscriber_data);
+}
+
+static void
+add_event_to_subscriber (GcalTimelineSubscriber *subscriber,
+                         GcalEvent              *event)
+{
+  GCAL_TRACE_MSG ("Adding event %s to subscriber %s",
+                  gcal_event_get_uid (event),
+                  G_OBJECT_TYPE_NAME (subscriber));
+
+  GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (subscriber)->add_event (subscriber, event);
+}
+
+static void
+update_subscriber_event (GcalTimelineSubscriber *subscriber,
+                         GcalEvent              *event)
+{
+  GCAL_TRACE_MSG ("Updating event '%s' (%s) at subscriber %s",
+                  gcal_event_get_summary (event),
+                  gcal_event_get_uid (event),
+                  G_OBJECT_TYPE_NAME (subscriber));
+
+  GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (subscriber)->update_event (subscriber, event);
+}
+static void
+remove_event_from_subscriber (GcalTimelineSubscriber *subscriber,
+                              GcalEvent              *event)
+{
+  GCAL_TRACE_MSG ("Removing event '%s' (%s) from subscriber %s",
+                  gcal_event_get_summary (event),
+                  gcal_event_get_uid (event),
+                  G_OBJECT_TYPE_NAME (subscriber));
+
+  GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (subscriber)->remove_event (subscriber, event);
+}
+
+static gboolean
+event_is_really_within_range (GcalEvent *event,
+                              GDateTime *range_start,
+                              GDateTime *range_end)
+{
+  GDateTime *event_start;
+  GDateTime *event_end;
+
+  /* Assume that ECalClientView did its job correctly */
+  if (!gcal_event_get_all_day (event))
+    return TRUE;
+
+  event_start = gcal_event_get_date_start (event);
+  event_end = gcal_event_get_date_end (event);
+
+  return gcal_date_time_compare_date (range_end, event_start) < 0 &&
+         gcal_date_time_compare_date (range_start, event_end) > 0;
+}
+
+static gboolean
+subscriber_contains_event (GcalTimelineSubscriber *subscriber,
+                           GcalEvent              *event)
+{
+  g_autoptr (GDateTime) subscriber_range_start = NULL;
+  g_autoptr (GDateTime) subscriber_range_end = NULL;
+
+  subscriber_range_start = gcal_timeline_subscriber_get_range_start (subscriber);
+  subscriber_range_end = gcal_timeline_subscriber_get_range_end (subscriber);
+
+  return event_is_really_within_range (event, subscriber_range_start, subscriber_range_end);
+}
+
+static void
+update_range (GcalTimeline *self)
+{
+  g_autoptr (GDateTime) new_range_start = NULL;
+  g_autoptr (GDateTime) new_range_end = NULL;
+  GcalTimelineSubscriber *subscriber;
+  GHashTableIter iter;
+  gboolean has_subscribers;
+  gboolean range_changed;
+
+  GCAL_ENTRY;
+
+  range_changed = FALSE;
+  has_subscribers = g_hash_table_size (self->subscribers) > 0;
+
+  if (has_subscribers)
+    {
+      g_hash_table_iter_init (&iter, self->subscribers);
+      while (g_hash_table_iter_next (&iter, (gpointer*) &subscriber, NULL))
+        {
+          g_autoptr (GDateTime) subscriber_start = NULL;
+          g_autoptr (GDateTime) subscriber_end = NULL;
+
+          subscriber_start = gcal_timeline_subscriber_get_range_start (subscriber);
+          subscriber_end = gcal_timeline_subscriber_get_range_end (subscriber);
+
+          if (!new_range_start || g_date_time_compare (subscriber_start, new_range_start) < 0)
+            {
+              gcal_clear_date_time (&new_range_start);
+              new_range_start = g_steal_pointer (&subscriber_start);
+            }
+
+          if (!new_range_end || g_date_time_compare (subscriber_end, new_range_end) > 0)
+            {
+              gcal_clear_date_time (&new_range_end);
+              new_range_end = g_steal_pointer (&subscriber_end);
+            }
+        }
+
+      if (!self->range_start || g_date_time_compare (self->range_start, new_range_start) != 0)
+        {
+          gcal_clear_date_time (&self->range_start);
+          self->range_start = g_steal_pointer (&new_range_start);
+          range_changed = TRUE;
+        }
+
+      if (!self->range_end || g_date_time_compare (self->range_end, new_range_end) != 0)
+        {
+          gcal_clear_date_time (&self->range_end);
+          self->range_end = g_steal_pointer (&new_range_end);
+          range_changed = TRUE;
+        }
+
+    }
+  else if (self->range_start || self->range_end)
+    {
+      gcal_clear_date_time (&self->range_start);
+      gcal_clear_date_time (&self->range_end);
+      range_changed = TRUE;
+    }
+
+  if (range_changed)
+    {
+      GcalCalendarMonitor *monitor;
+
+      g_hash_table_iter_init (&iter, self->calendars);
+      while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &monitor))
+        gcal_calendar_monitor_set_range (monitor, self->range_start, self->range_end);
+    }
+
+  GCAL_EXIT;
+}
+
+static void
+calculate_changed_events (GcalTimeline            *self,
+                          GcalTimelineSubscriber  *subscriber,
+                          GDateTime               *old_range_start,
+                          GDateTime               *old_range_end,
+                          GDateTime               *new_range_start,
+                          GDateTime               *new_range_end)
+{
+  g_autoptr (GPtrArray) events_to_remove = NULL;
+  g_autoptr (GPtrArray) events_to_add = NULL;
+  gint range_diff;
+  gint i;
+
+  events_to_add = g_ptr_array_new ();
+  events_to_remove = g_ptr_array_new ();
+
+  /* Start ranges diff */
+  range_diff = g_date_time_compare (old_range_start, new_range_start);
+  if (range_diff != 0)
+    {
+      g_autoptr (GPtrArray) events = NULL;
+
+      if (range_diff < 0)
+        {
+          /* Removed */
+          events = gcal_range_tree_get_data_at_range (self->events, old_range_start, new_range_start);
+          if (events)
+            g_ptr_array_extend (events_to_remove, events, NULL, NULL);
+        }
+      else if (range_diff > 0)
+        {
+          /* Added */
+          events = gcal_range_tree_get_data_at_range (self->events, new_range_start, old_range_start);
+          if (events)
+            g_ptr_array_extend (events_to_add, events, NULL, NULL);
+        }
+    }
+
+  /* End ranges diff */
+  range_diff = g_date_time_compare (old_range_end, new_range_end);
+  if (range_diff != 0)
+    {
+      g_autoptr (GPtrArray) events = NULL;
+
+      if (range_diff < 0)
+        {
+          events = gcal_range_tree_get_data_at_range (self->events, old_range_end, new_range_end);
+          if (events)
+            g_ptr_array_extend (events_to_add, events, NULL, NULL);
+        }
+      else if (range_diff > 0)
+        {
+          events = gcal_range_tree_get_data_at_range (self->events, new_range_end, old_range_end);
+          if (events)
+            g_ptr_array_extend (events_to_remove, events, NULL, NULL);
+        }
+    }
+
+  for (i = 0; i < events_to_remove->len; i++)
+    {
+      GcalEvent *event = g_ptr_array_index (events_to_remove, i);
+
+      if (!event_is_really_within_range (event, old_range_start, old_range_end))
+        continue;
+
+      remove_event_from_subscriber (subscriber, event);
+    }
+
+  for (i = 0; i < events_to_add->len; i++)
+    {
+      GcalEvent *event = g_ptr_array_index (events_to_add, i);
+
+      if (!event_is_really_within_range (event, new_range_start, new_range_end))
+        continue;
+
+      add_event_to_subscriber (subscriber, event);
+    }
+}
+
+static void
+update_subscriber_range (GcalTimeline           *self,
+                         GcalTimelineSubscriber *subscriber)
+{
+  g_autoptr (GDateTime) new_range_start = NULL;
+  g_autoptr (GDateTime) new_range_end = NULL;
+  SubscriberData *subscriber_data;
+
+
+  GCAL_ENTRY;
+
+  subscriber_data = g_hash_table_lookup (self->subscribers, subscriber);
+  g_assert (subscriber_data != NULL);
+
+  new_range_start = gcal_timeline_subscriber_get_range_start (subscriber);
+  new_range_end = gcal_timeline_subscriber_get_range_end (subscriber);
+
+  /* Diff new and old event ranges */
+  calculate_changed_events (self,
+                            subscriber,
+                            subscriber_data->range_start,
+                            subscriber_data->range_end,
+                            new_range_start,
+                            new_range_end);
+
+  /* Update the subscriber range */
+  gcal_range_tree_remove_data (self->subscriber_ranges, subscriber);
+  gcal_range_tree_add_range (self->subscriber_ranges,
+                             new_range_start,
+                             new_range_end,
+                             subscriber);
+
+  gcal_set_date_time (&subscriber_data->range_start, new_range_start);
+  gcal_set_date_time (&subscriber_data->range_end, new_range_end);
+
+  GCAL_EXIT;
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_calendar_monitor_event_added_cb (GcalCalendarMonitor *monitor,
+                                    GcalEvent           *event,
+                                    GcalTimeline        *self)
+{
+  g_autoptr (GPtrArray) subscribers_at_range = NULL;
+  GDateTime *event_start;
+  GDateTime *event_end;
+  gint i;
+
+  GCAL_ENTRY;
+
+  event_start = gcal_event_get_date_start (event);
+  event_end = gcal_event_get_date_end (event);
+
+  gcal_range_tree_add_range (self->events, event_start, event_end, g_object_ref (event));
+
+  /* Add to all subscribers within the event range */
+  subscribers_at_range = gcal_range_tree_get_data_at_range (self->subscriber_ranges, event_start, event_end);
+  for (i = 0; subscribers_at_range && i < subscribers_at_range->len; i++)
+    {
+      GcalTimelineSubscriber *subscriber = g_ptr_array_index (subscribers_at_range, i);
+
+      if (!subscriber_contains_event (subscriber, event))
+        continue;
+
+      add_event_to_subscriber (subscriber, event);
+    }
+
+  GCAL_EXIT;
+}
+
+static void
+on_calendar_monitor_event_updated_cb (GcalCalendarMonitor *monitor,
+                                      GcalEvent           *old_event,
+                                      GcalEvent           *event,
+                                      GcalTimeline        *self)
+{
+  g_autoptr (GPtrArray) subscribers_at_range = NULL;
+  GDateTime *old_event_start;
+  GDateTime *old_event_end;
+  GDateTime *event_start;
+  GDateTime *event_end;
+  gint i;
+
+  GCAL_ENTRY;
+
+  /* Remove the old event */
+  old_event_start = gcal_event_get_date_start (old_event);
+  old_event_end = gcal_event_get_date_end (old_event);
+
+  gcal_range_tree_remove_range (self->events, old_event_start, old_event_end, old_event);
+
+  /* Add the updated event to the (potentially new) range */
+  event_start = gcal_event_get_date_start (event);
+  event_end = gcal_event_get_date_end (event);
+
+  gcal_range_tree_add_range (self->events, event_start, event_end, g_object_ref (event));
+
+  /* Add to all subscribers within the event range */
+  subscribers_at_range = gcal_range_tree_get_data_at_range (self->subscriber_ranges, event_start, event_end);
+  for (i = 0; subscribers_at_range && i < subscribers_at_range->len; i++)
+    {
+      GcalTimelineSubscriber *subscriber = g_ptr_array_index (subscribers_at_range, i);
+
+      if (!subscriber_contains_event (subscriber, event))
+        continue;
+
+      update_subscriber_event (subscriber, event);
+    }
+
+  GCAL_EXIT;
+}
+
+static void
+on_calendar_monitor_event_removed_cb (GcalCalendarMonitor *monitor,
+                                      GcalEvent           *event,
+                                      GcalTimeline        *self)
+{
+  g_autoptr (GPtrArray) subscribers_at_range = NULL;
+  GDateTime *event_start;
+  GDateTime *event_end;
+  gint i;
+
+  GCAL_ENTRY;
+
+  event_start = gcal_event_get_date_start (event);
+  event_end = gcal_event_get_date_end (event);
+
+  /* Add to all subscribers within the event range */
+  subscribers_at_range = gcal_range_tree_get_data_at_range (self->subscriber_ranges, event_start, event_end);
+  for (i = 0; subscribers_at_range && i < subscribers_at_range->len; i++)
+    {
+      GcalTimelineSubscriber *subscriber = g_ptr_array_index (subscribers_at_range, i);
+
+      if (!subscriber_contains_event (subscriber, event))
+        continue;
+
+      remove_event_from_subscriber (subscriber, event);
+    }
+
+  gcal_range_tree_remove_range (self->events, event_start, event_end, event);
+
+  GCAL_EXIT;
+}
+
+static gboolean
+update_timeline_range_in_idle_cb (gpointer user_data)
+{
+  GcalTimeline *self;
+
+  GCAL_ENTRY;
+
+  self = GCAL_TIMELINE (user_data);
+
+  update_range (self);
+
+  self->update_range_idle_id = 0;
+
+  GCAL_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+on_subscriber_range_changed_cb (GcalTimelineSubscriber *subscriber,
+                                GcalTimeline           *self)
+{
+  update_subscriber_range (self, subscriber);
+
+  if (self->update_range_idle_id)
+    return;
+
+  self->update_range_idle_id = g_idle_add (update_timeline_range_in_idle_cb, self);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gcal_timeline_finalize (GObject *object)
+{
+  GcalTimeline *self = (GcalTimeline *)object;
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+
+  if (self->update_range_idle_id > 0)
+    {
+      g_source_remove (self->update_range_idle_id);
+      self->update_range_idle_id = 0;
+    }
+
+  g_clear_pointer (&self->events, gcal_range_tree_unref);
+  g_clear_pointer (&self->calendars, g_hash_table_destroy);
+  g_clear_pointer (&self->subscribers, g_hash_table_destroy);
+  g_clear_pointer (&self->subscriber_ranges, gcal_range_tree_unref);
+
+  G_OBJECT_CLASS (gcal_timeline_parent_class)->finalize (object);
+}
+
+static void
+gcal_timeline_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  GcalTimeline *self = GCAL_TIMELINE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, self->context);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_timeline_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  GcalTimeline *self = GCAL_TIMELINE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_assert (self->context == NULL);
+      self->context = g_value_get_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_timeline_class_init (GcalTimelineClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gcal_timeline_finalize;
+  object_class->get_property = gcal_timeline_get_property;
+  object_class->set_property = gcal_timeline_set_property;
+
+  /**
+   * GcalSearchEngine::context:
+   *
+   * The #GcalContext of the application.
+   */
+  properties[PROP_CONTEXT] = g_param_spec_object ("context",
+                                                  "Data context",
+                                                  "Data context",
+                                                  GCAL_TYPE_CONTEXT,
+                                                  G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gcal_timeline_init (GcalTimeline *self)
+{
+  self->cancellable = g_cancellable_new ();
+  self->events = gcal_range_tree_new_with_free_func (g_object_unref);
+  self->calendars = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref);
+  self->subscribers = g_hash_table_new_full (NULL, NULL, g_object_unref, (GDestroyNotify) 
subscriber_data_free);
+  self->subscriber_ranges = gcal_range_tree_new ();
+}
+
+/**
+ * gcal_timeline_new:
+ * @context: a #GcalContext
+ *
+ * Creates a new #GcalTimeline.
+ *
+ * Returns: (transfer full): a #GcalTimeline
+ */
+GcalTimeline*
+gcal_timeline_new (GcalContext *context)
+{
+  return g_object_new (GCAL_TYPE_TIMELINE,
+                       "context", context,
+                       NULL);
+}
+
+/**
+ * gcal_timeline_add_calendar:
+ * @self: a #GcalTimeline
+ * @calendar: a #GcalCalendar
+ *
+ * Adds @calendar to @self. The events in @calendar will be added
+ * respectively to
+ *
+ */
+void
+gcal_timeline_add_calendar (GcalTimeline *self,
+                            GcalCalendar *calendar)
+{
+  g_autoptr (GcalCalendarMonitor) monitor = NULL;
+
+  g_return_if_fail (GCAL_IS_TIMELINE (self));
+  g_return_if_fail (GCAL_IS_CALENDAR (calendar));
+
+  GCAL_ENTRY;
+
+  if (g_hash_table_contains (self->calendars, calendar))
+    return;
+
+  GCAL_TRACE_MSG ("Adding calendar '%s' to timeline %p", gcal_calendar_get_name (calendar), self);
+
+  monitor = gcal_calendar_monitor_new (calendar);
+  g_signal_connect (monitor, "event-added", G_CALLBACK (on_calendar_monitor_event_added_cb), self);
+  g_signal_connect (monitor, "event-updated", G_CALLBACK (on_calendar_monitor_event_updated_cb), self);
+  g_signal_connect (monitor, "event-removed", G_CALLBACK (on_calendar_monitor_event_removed_cb), self);
+  g_hash_table_insert (self->calendars, calendar, g_object_ref (monitor));
+
+  if (self->range_start && self->range_end)
+    gcal_calendar_monitor_set_range (monitor, self->range_start, self->range_end);
+
+  GCAL_EXIT;
+}
+
+void
+gcal_timeline_remove_calendar (GcalTimeline *self,
+                               GcalCalendar *calendar)
+{
+  g_return_if_fail (GCAL_IS_TIMELINE (self));
+  g_return_if_fail (GCAL_IS_CALENDAR (calendar));
+
+  GCAL_ENTRY;
+
+  g_object_ref (calendar);
+
+  if (g_hash_table_remove (self->calendars, calendar))
+    GCAL_TRACE_MSG ("Removing calendar '%s' from timeline %p", gcal_calendar_get_name (calendar), self);
+
+  g_object_unref (calendar);
+
+  GCAL_EXIT;
+}
+
+void
+gcal_timeline_add_subscriber (GcalTimeline           *self,
+                              GcalTimelineSubscriber *subscriber)
+{
+  SubscriberData *subscriber_data;
+
+  g_return_if_fail (GCAL_IS_TIMELINE (self));
+  g_return_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (subscriber));
+
+  GCAL_ENTRY;
+
+  if (g_hash_table_contains (self->subscribers, subscriber))
+    GCAL_RETURN ();
+
+  g_debug ("Adding subscriber %s to timeline %p", G_OBJECT_TYPE_NAME (subscriber), self);
+
+  subscriber_data = subscriber_data_new (subscriber);
+
+  g_hash_table_insert (self->subscribers, g_object_ref (subscriber), g_steal_pointer (&subscriber_data));
+  g_signal_connect_object (subscriber,
+                           "range-changed",
+                           G_CALLBACK (on_subscriber_range_changed_cb),
+                           self,
+                           0);
+
+  update_subscriber_range (self, subscriber);
+  update_range (self);
+
+  GCAL_EXIT;
+}
+
+void
+gcal_timeline_remove_subscriber (GcalTimeline           *self,
+                                 GcalTimelineSubscriber *subscriber)
+{
+  g_return_if_fail (GCAL_IS_TIMELINE (self));
+  g_return_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (subscriber));
+
+  GCAL_ENTRY;
+
+  if (!g_hash_table_contains (self->subscribers, subscriber))
+    GCAL_RETURN ();
+
+  g_debug ("Removing subscriber %s from timeline %p", G_OBJECT_TYPE_NAME (subscriber), self);
+
+  g_signal_handlers_disconnect_by_func (subscriber, on_subscriber_range_changed_cb, self);
+  g_hash_table_remove (self->subscribers, subscriber);
+
+  update_range (self);
+
+  GCAL_EXIT;
+}
diff --git a/src/core/gcal-timeline.h b/src/core/gcal-timeline.h
new file mode 100644
index 00000000..ccaa2e73
--- /dev/null
+++ b/src/core/gcal-timeline.h
@@ -0,0 +1,46 @@
+/* gcal-timeline.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "gcal-types.h"
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_TIMELINE (gcal_timeline_get_type())
+G_DECLARE_FINAL_TYPE (GcalTimeline, gcal_timeline, GCAL, TIMELINE, GObject)
+
+GcalTimeline*        gcal_timeline_new                           (GcalContext        *context);
+
+void                 gcal_timeline_add_calendar                  (GcalTimeline       *self,
+                                                                  GcalCalendar       *calendar);
+
+void                 gcal_timeline_remove_calendar               (GcalTimeline       *self,
+                                                                  GcalCalendar       *calendar);
+
+void                 gcal_timeline_add_subscriber                (GcalTimeline           *self,
+                                                                  GcalTimelineSubscriber *subscriber);
+
+void                 gcal_timeline_remove_subscriber             (GcalTimeline           *self,
+                                                                  GcalTimelineSubscriber *subscriber);
+
+G_END_DECLS
diff --git a/src/gcal-types.h b/src/gcal-types.h
index 3746f05a..fc3a2941 100644
--- a/src/gcal-types.h
+++ b/src/gcal-types.h
@@ -24,6 +24,10 @@
 
 G_BEGIN_DECLS
 
+typedef struct _GcalCalendar GcalCalendar;
 typedef struct _GcalContext GcalContext;
+typedef struct _GcalEvent GcalEvent;
+typedef struct _GcalTimeline GcalTimeline;
+typedef struct _GcalTimelineSubscriber GcalTimelineSubscriber;
 
 G_END_DECLS
diff --git a/src/meson.build b/src/meson.build
index 2244b076..43187dcf 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -93,6 +93,7 @@ calendar_incs += contrib_incs
 
 sources = files(
   'core/gcal-calendar.c',
+  'core/gcal-calendar-monitor.c',
   'core/gcal-clock.c',
   'core/gcal-context.c',
   'core/gcal-event.c',
@@ -101,6 +102,8 @@ sources = files(
   'core/gcal-night-light-monitor.c',
   'core/gcal-recurrence.c',
   'core/gcal-shell-search-provider.c',
+  'core/gcal-timeline.c',
+  'core/gcal-timeline-subscriber.c',
   'core/gcal-timer.c',
   'core/gcal-time-zone-monitor.c',
   'gui/calendar-management/gcal-calendar-management-dialog.c',


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