[evolution-data-server] Move evolution-alarm-notify to evolution-data-server



commit 8ba0b06b72e96604db51be8c032afd33fcabe400
Author: Milan Crha <mcrha redhat com>
Date:   Fri May 11 12:50:54 2018 +0200

    Move evolution-alarm-notify to evolution-data-server
    
    Apart of the move itself, it also contains a UI change of the notification
    dialog, the same as the changed way of dealing with the reminders:
    a) reminders persist between sessions, until they are dismissed
    b) snoozed reminders also persist between sessions.

 CMakeLists.txt                                     |   13 +
 config.h.in                                        |    3 +
 data/CMakeLists.txt                                |   18 +
 .../org.gnome.Evolution-alarm-notify.desktop.in.in |   15 +
 ...e.evolution-data-server.calendar.gschema.xml.in |   48 +
 evolution-data-server.pc.in                        |    1 +
 po/POTFILES.in                                     |    4 +
 src/calendar/libecal/e-cal-util.c                  |   72 +
 src/calendar/libecal/e-cal-util.h                  |    2 +
 src/calendar/libecal/e-reminder-watcher.c          |  563 ++++++-
 src/calendar/libecal/e-reminder-watcher.h          |   36 +-
 src/libedataserver/CMakeLists.txt                  |    1 +
 src/libedataserver/e-data-server-util.c            |    3 +
 src/libedataserver/libedataserver-private.h        |    4 +
 src/libedataserverui/CMakeLists.txt                |   10 +
 src/libedataserverui/e-reminders-widget.c          | 1813 ++++++++++++++++++++
 src/libedataserverui/e-reminders-widget.h          |  109 ++
 src/libedataserverui/libedataserverui-private.c    |   49 +
 src/libedataserverui/libedataserverui-private.h    |   29 +
 src/libedataserverui/libedataserverui.h            |    1 +
 src/libedataserverui/libedataserverui.pc.in        |    3 +-
 src/services/CMakeLists.txt                        |    1 +
 src/services/evolution-alarm-notify/CMakeLists.txt |   67 +
 .../evolution-alarm-notify/e-alarm-notify.c        | 1105 ++++++++++++
 .../evolution-alarm-notify/e-alarm-notify.h        |   66 +
 .../evolution-alarm-notify-icon.rc                 |    1 +
 .../evolution-alarm-notify.c                       |  106 ++
 .../evolution-alarm-notify.ico                     |  Bin 0 -> 17542 bytes
 28 files changed, 4083 insertions(+), 60 deletions(-)
---
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2e43fb8..f419db8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -120,6 +120,7 @@ set(libaccounts_glib_minimum_version 1.4)
 set(libsignon_glib_minimum_version 1.8)
 set(json_glib_minimum_version 1.0.4)
 set(webkit2gtk_minimum_version 2.11.91)
+set(libcanberra_gtk_minimum_version 0.25)
 
 # Load modules from the source tree
 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
@@ -195,6 +196,7 @@ endif(WIN32)
 set(imagesdir "${SHARE_INSTALL_PREFIX}/pixmaps/${PROJECT_NAME}")
 set(moduledir "${privlibdir}/registry-modules")
 set(credentialmoduledir "${privlibdir}/credential-modules")
+set(uimoduledir "${privlibdir}/ui-modules")
 set(ebook_backenddir "${privlibdir}/addressbook-backends")
 set(ecal_backenddir "${privlibdir}/calendar-backends")
 set(ro_sourcesdir "${privdatadir}/ro-sources")
@@ -755,6 +757,17 @@ if(ENABLE_WEATHER)
        unset(CMAKE_REQUIRED_LIBRARIES)
 endif(ENABLE_WEATHER)
 
+# ************************************************
+# evolution-alarm-notify : Canberra-GTK for Sound
+# ************************************************
+
+add_printable_option(ENABLE_CANBERRA "Enable Canberra-GTK for sound in evolution-alarm-notify" ON)
+
+if(ENABLE_CANBERRA)
+       pkg_check_modules_for_option(ENABLE_CANBERRA "Canberra-GTK for sound in evolution-alarm-notify" 
CANBERRA libcanberra-gtk3>=${libcanberra_gtk_minimum_version})
+       set(HAVE_CANBERRA ON)
+endif(ENABLE_CANBERRA)
+
 # ******************************
 # File locking
 # ******************************
diff --git a/config.h.in b/config.h.in
index d39555b..99481e5 100644
--- a/config.h.in
+++ b/config.h.in
@@ -199,3 +199,6 @@
 
 /* gweather_info_new() has only one argument */
 #cmakedefine HAVE_ONE_ARG_GWEATHER_INFO_NEW 1
+
+/* evolution-alarm-notify - Define if using Canberra-GTK for sound */
+#cmakedefine HAVE_CANBERRA 1
diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt
index e1714ea..e1be691 100644
--- a/data/CMakeLists.txt
+++ b/data/CMakeLists.txt
@@ -1,4 +1,21 @@
 # ********************************
+# evolution-alarm-notify
+# ********************************
+
+set(autostartdir ${SYSCONF_INSTALL_DIR}/xdg/autostart)
+
+configure_file(org.gnome.Evolution-alarm-notify.desktop.in.in
+       org.gnome.Evolution-alarm-notify.desktop.in
+       @ONLY
+)
+
+intltool_merge(${CMAKE_CURRENT_BINARY_DIR}/org.gnome.Evolution-alarm-notify.desktop.in 
org.gnome.Evolution-alarm-notify.desktop --desktop-style --utf8)
+
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.gnome.Evolution-alarm-notify.desktop
+       DESTINATION ${autostartdir}
+)
+
+# ********************************
 # GSettings schemas
 # ********************************
 
@@ -23,6 +40,7 @@ add_custom_command(OUTPUT gschemas.compiled
 
 add_custom_target(data-files ALL
        DEPENDS gschemas.compiled
+               org.gnome.Evolution-alarm-notify.desktop
 )
 
 add_gsettings_schemas(data-files ${BUILT_SCHEMAS})
diff --git a/data/org.gnome.Evolution-alarm-notify.desktop.in.in 
b/data/org.gnome.Evolution-alarm-notify.desktop.in.in
new file mode 100644
index 0000000..b0d76b0
--- /dev/null
+++ b/data/org.gnome.Evolution-alarm-notify.desktop.in.in
@@ -0,0 +1,15 @@
+[Desktop Entry]
+Type=Application
+_Name=Evolution Alarm Notify
+_Comment=Calendar event notifications
+Icon=appointment-soon
+Exec=@privlibexecdir@/evolution-alarm-notify
+Terminal=false
+Categories=
+OnlyShowIn=GNOME;Unity;XFCE;Dawati;MATE;
+NoDisplay=true
+X-Meego-Priority=Low
+X-GNOME-Bugzilla-Bugzilla=GNOME
+X-GNOME-Bugzilla-Product=evolution-data-server
+X-GNOME-Bugzilla-Component=calendar
+X-GNOME-Bugzilla-Version=@BASE_VERSION@.x
diff --git a/data/org.gnome.evolution-data-server.calendar.gschema.xml.in 
b/data/org.gnome.evolution-data-server.calendar.gschema.xml.in
index 2e82c97..4878eb5 100644
--- a/data/org.gnome.evolution-data-server.calendar.gschema.xml.in
+++ b/data/org.gnome.evolution-data-server.calendar.gschema.xml.in
@@ -32,5 +32,53 @@
       <default>['']</default>
       <_summary>Snoozed reminders for EReminderWatcher</_summary>
     </key>
+
+    <key name="notify-programs" type="as">
+      <default>[]</default>
+      <_summary>Reminder programs</_summary>
+      <_description>Programs that are allowed to be run by reminders</_description>
+    </key>
+    <key name="notify-with-tray" type="b">
+      <default>true</default>
+      <_summary>Show reminders in notification tray only</_summary>
+      <_description>When set to true, the reminders are shown only in the notification tray, otherwise the 
reminders dialog is shown immediately</_description>
+    </key>
+    <key name="notify-window-on-top" type="b">
+      <default>true</default>
+      <_summary>Show reminder notification dialog always on top</_summary>
+      <_description>Whether or not to show reminder notification dialog always on top. Note this works only 
as a hint for the window manager, which may or may not obey it.</_description>
+    </key>
+    <key name="notify-window-x" type="i">
+      <default>-1</default>
+      <_summary>X position of the reminder notification dialog</_summary>
+    </key>
+    <key name="notify-window-y" type="i">
+      <default>-1</default>
+      <_summary>Y position of the reminder notification dialog</_summary>
+    </key>
+    <key name="notify-window-width" type="i">
+      <default>-1</default>
+      <_summary>Width of the reminder notification dialog</_summary>
+    </key>
+    <key name="notify-window-height" type="i">
+      <default>-1</default>
+      <_summary>Height of the reminder notification dialog</_summary>
+    </key>
+    <key name="notify-completed-tasks" type="b">
+      <default>true</default>
+      <_summary>Show reminder notification for completed tasks</_summary>
+    </key>
+    <key name="notify-past-events" type="b">
+      <default>true</default>
+      <_summary>Show reminder notification for past events</_summary>
+    </key>
+    <key name="notify-last-snooze-minutes" type="i">
+      <default>5</default>
+      <_summary>The last used snooze time, in minutes</_summary>
+    </key>
+    <key name="notify-custom-snooze-minutes" type="ai">
+      <default>[]</default>
+      <_summary>User-defined snooze times, in minutes</_summary>
+    </key>
   </schema>
 </schemalist>
diff --git a/evolution-data-server.pc.in b/evolution-data-server.pc.in
index 319735b..d3d5823 100644
--- a/evolution-data-server.pc.in
+++ b/evolution-data-server.pc.in
@@ -5,6 +5,7 @@ privlibdir=@privlibdir@
 datarootdir=@SHARE_INSTALL_PREFIX@
 datadir=@SHARE_INSTALL_PREFIX@
 privdatadir=@privdatadir@
+privlibexecdir=@privlibexecdir@
 
 addressbookdbusservicename=@ADDRESS_BOOK_DBUS_SERVICE_NAME@
 calendardbusservicename=@CALENDAR_DBUS_SERVICE_NAME@
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 3c1450d..de740a4 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -37,6 +37,7 @@ src/calendar/libecal/e-cal-client-view.c
 src/calendar/libecal/e-cal-component.c
 src/calendar/libecal/e-cal-recur.c
 src/calendar/libecal/e-cal-util.c
+src/calendar/libecal/e-reminder-watcher.c
 src/calendar/libedata-cal/e-cal-backend.c
 src/calendar/libedata-cal/e-cal-backend-sexp.c
 src/calendar/libedata-cal/e-cal-backend-sync.c
@@ -183,6 +184,7 @@ data/org.gnome.evolution-data-server.addressbook.gschema.xml.in
 data/org.gnome.evolution-data-server.calendar.gschema.xml.in
 data/org.gnome.evolution-data-server.gschema.xml.in
 data/org.gnome.evolution.shell.network-config.gschema.xml.in
+data/org.gnome.Evolution-alarm-notify.desktop.in.in
 src/libebackend/e-backend.c
 src/libebackend/e-cache.c
 src/libebackend/e-collection-backend.c
@@ -210,6 +212,7 @@ src/libedataserver/e-webdav-session.c
 src/libedataserverui/e-credentials-prompter.c
 src/libedataserverui/e-credentials-prompter-impl-oauth2.c
 src/libedataserverui/e-credentials-prompter-impl-password.c
+src/libedataserverui/e-reminders-widget.c
 src/libedataserverui/e-trust-prompt.c
 src/libedataserverui/e-webdav-discover-widget.c
 src/modules/gnome-online-accounts/e-goa-password-based.c
@@ -233,6 +236,7 @@ src/modules/ubuntu-online-accounts/uoa-utils.c
 [type: gettext/xml]src/modules/ubuntu-online-accounts/yahoo-mail.service.in.in
 src/modules/yahoo-backend/module-yahoo-backend.c
 src/services/evolution-addressbook-factory/evolution-addressbook-factory.c
+src/services/evolution-alarm-notify/e-alarm-notify.c
 src/services/evolution-calendar-factory/evolution-calendar-factory.c
 [type: gettext/ini]src/services/evolution-source-registry/builtin/birthdays.source.in
 [type: gettext/ini]src/services/evolution-source-registry/builtin/caldav-stub.source.in
diff --git a/src/calendar/libecal/e-cal-util.c b/src/calendar/libecal/e-cal-util.c
index 8c45dab..c590828 100644
--- a/src/calendar/libecal/e-cal-util.c
+++ b/src/calendar/libecal/e-cal-util.c
@@ -744,6 +744,78 @@ e_cal_util_priority_from_string (const gchar *string)
        return priority;
 }
 
+/**
+ * e_cal_util_seconds_to_string:
+ * @seconds: actual time, in seconds
+ *
+ * Converts time, in seconds, into a string representation readable by humans
+ * and localized into the current locale. This can be used to convert event
+ * duration to string or similar use cases.
+ *
+ * Free the returned string with g_free(), when no longer needed.
+ *
+ * Returns: (transfer full): a newly allocated string with localized description
+ *    of the given time in seconds.
+ *
+ * Since: 3.30
+ **/
+gchar *
+e_cal_util_seconds_to_string (gint64 seconds)
+{
+       gchar *times[6], *text;
+       gint ii;
+
+       ii = 0;
+       if (seconds >= 7 * 24 * 3600) {
+               gint weeks;
+
+               weeks = seconds / (7 * 24 * 3600);
+               seconds %= (7 * 24 * 3600);
+
+               times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d week", "%d weeks", weeks), 
weeks);
+       }
+
+       if (seconds >= 24 * 3600) {
+               gint days;
+
+               days = seconds / (24 * 3600);
+               seconds %= (24 * 3600);
+
+               times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d day", "%d days", days), 
days);
+       }
+
+       if (seconds >= 3600) {
+               gint hours;
+
+               hours = seconds / 3600;
+               seconds %= 3600;
+
+               times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d hour", "%d hours", hours), 
hours);
+       }
+
+       if (seconds >= 60) {
+               gint minutes;
+
+               minutes = seconds / 60;
+               seconds %= 60;
+
+               times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d minute", "%d minutes", 
minutes), minutes);
+       }
+
+       if (seconds != 0) {
+               /* Translators: here, "second" is the time division (like "minute"), not the ordinal number 
(like "third") */
+               times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d second", "%d seconds", 
seconds), (gint) seconds);
+       }
+
+       times[ii] = NULL;
+       text = g_strjoinv (" ", times);
+       while (ii > 0) {
+               g_free (times[--ii]);
+       }
+
+       return text;
+}
+
 /* callback for icalcomponent_foreach_tzid */
 typedef struct {
        icalcomponent *vcal_comp;
diff --git a/src/calendar/libecal/e-cal-util.h b/src/calendar/libecal/e-cal-util.h
index d43f1bf..e966614 100644
--- a/src/calendar/libecal/e-cal-util.h
+++ b/src/calendar/libecal/e-cal-util.h
@@ -81,6 +81,8 @@ gint          e_cal_util_generate_alarms_for_list
 const gchar *  e_cal_util_priority_to_string   (gint priority);
 gint           e_cal_util_priority_from_string (const gchar *string);
 
+gchar *                e_cal_util_seconds_to_string    (gint64 seconds);
+
 void           e_cal_util_add_timezones_from_component
                                                (icalcomponent *vcal_comp,
                                                 icalcomponent *icalcomp);
diff --git a/src/calendar/libecal/e-reminder-watcher.c b/src/calendar/libecal/e-reminder-watcher.c
index 56c70c5..c8eeed7 100644
--- a/src/calendar/libecal/e-reminder-watcher.c
+++ b/src/calendar/libecal/e-reminder-watcher.c
@@ -32,11 +32,14 @@
 #include "evolution-data-server-config.h"
 
 #include <string.h>
+#include <glib/gi18n-lib.h>
 
 #include "libedataserver/libedataserver.h"
 
 #include "e-cal-client.h"
+#include "e-cal-system-timezone.h"
 #include "e-cal-time-util.h"
+#include "e-cal-util.h"
 
 #include "e-reminder-watcher.h"
 
@@ -53,6 +56,7 @@ struct _EReminderWatcherPrivate {
        ESourceRegistryWatcher *registry_watcher;
        GCancellable *cancellable;
        GSettings *settings;
+       gboolean timers_enabled;
        gulong past_changed_handler_id;
        gulong snoozed_changed_handler_id;
        guint expected_past_changes;
@@ -77,12 +81,13 @@ struct _EReminderWatcherPrivate {
 enum {
        PROP_0,
        PROP_REGISTRY,
-       PROP_DEFAULT_ZONE
+       PROP_DEFAULT_ZONE,
+       PROP_TIMERS_ENABLED
 };
 
 enum {
+       FORMAT_TIME,
        TRIGGERED,
-       REMOVED,
        CHANGED,
        LAST_SIGNAL
 };
@@ -1028,7 +1033,7 @@ e_reminder_watcher_calc_next_midnight (EReminderWatcher *watcher)
                e_reminder_watcher_debug_print ("Required correction of the day end, now at %s\n", 
e_reminder_watcher_timet_as_string ((gint64) midnight));
        }
 
-       if (watcher->priv->next_midnight != midnight) {
+       if (watcher->priv->next_midnight != midnight && watcher->priv->timers_enabled) {
                GSList *link;
 
                e_reminder_watcher_debug_print ("Next midnight at %s\n", e_reminder_watcher_timet_as_string 
((gint64) midnight));
@@ -1084,6 +1089,26 @@ e_reminder_watcher_schedule_timer_impl (EReminderWatcher *watcher,
        g_rec_mutex_unlock (&watcher->priv->lock);
 }
 
+static void
+e_reminder_watcher_format_time_impl (EReminderWatcher *watcher,
+                                    const EReminderData *rd,
+                                    struct icaltimetype *itt,
+                                    gchar **inout_buffer,
+                                    gint buffer_size)
+{
+       struct tm tm;
+
+       g_return_if_fail (E_IS_REMINDER_WATCHER (watcher));
+       g_return_if_fail (rd != NULL);
+       g_return_if_fail (itt != NULL);
+       g_return_if_fail (inout_buffer != NULL);
+       g_return_if_fail (*inout_buffer != NULL);
+       g_return_if_fail (buffer_size > 0);
+
+       tm = icaltimetype_to_tm (itt);
+       e_time_format_date_and_time (&tm, FALSE, FALSE, FALSE, *inout_buffer, buffer_size);
+}
+
 static GSList * /* EReminderData * */
 e_reminder_watcher_reminders_from_key (EReminderWatcher *watcher,
                                       const gchar *key)
@@ -1191,6 +1216,7 @@ typedef struct _EmitSignalData {
        EReminderWatcher *watcher;
        guint signal_id;
        GSList *reminders; /* EReminderData * */
+       gboolean is_snoozed; /* only for the triggered signal */
 } EmitSignalData;
 
 static void
@@ -1213,7 +1239,10 @@ e_reminder_watcher_emit_signal_idle_cb (gpointer user_data)
        g_return_val_if_fail (esd != NULL, FALSE);
        g_return_val_if_fail (E_IS_REMINDER_WATCHER (esd->watcher), FALSE);
 
-       g_signal_emit (esd->watcher, esd->signal_id, 0, esd->reminders, NULL);
+       if (esd->signal_id == signals[TRIGGERED])
+               g_signal_emit (esd->watcher, esd->signal_id, 0, esd->reminders, esd->is_snoozed, NULL);
+       else
+               g_signal_emit (esd->watcher, esd->signal_id, 0, esd->reminders, NULL);
 
        return FALSE;
 }
@@ -1221,7 +1250,8 @@ e_reminder_watcher_emit_signal_idle_cb (gpointer user_data)
 static void
 e_reminder_watcher_emit_signal_idle_multiple (EReminderWatcher *watcher,
                                              guint signal_id,
-                                             const GSList *reminders) /* EReminderData * */
+                                             const GSList *reminders, /* EReminderData * */
+                                             gboolean is_snoozed)
 {
        EmitSignalData *esd;
 
@@ -1229,6 +1259,7 @@ e_reminder_watcher_emit_signal_idle_multiple (EReminderWatcher *watcher,
        esd->watcher = g_object_ref (watcher);
        esd->signal_id = signal_id;
        esd->reminders = g_slist_copy_deep ((GSList *) reminders, (GCopyFunc) e_reminder_data_copy, NULL);
+       esd->is_snoozed = is_snoozed;
 
        g_idle_add_full (G_PRIORITY_HIGH_IDLE, e_reminder_watcher_emit_signal_idle_cb, esd, 
emit_signal_data_free);
 }
@@ -1243,7 +1274,7 @@ e_reminder_watcher_emit_signal_idle (EReminderWatcher *watcher,
        if (rd)
                reminders = g_slist_prepend (NULL, e_reminder_data_copy (rd));
 
-       e_reminder_watcher_emit_signal_idle_multiple (watcher, signal_id, reminders);
+       e_reminder_watcher_emit_signal_idle_multiple (watcher, signal_id, reminders, FALSE);
 
        g_slist_free_full (reminders, e_reminder_data_free);
 }
@@ -1299,8 +1330,6 @@ e_reminder_watcher_remove_from_past (EReminderWatcher *watcher,
 
                e_reminder_watcher_save_past (watcher, reminders);
 
-               e_reminder_watcher_emit_signal_idle (watcher, signals[REMOVED], found);
-
                e_reminder_watcher_debug_print ("Removed reminder from past for '%s' from %s at %s\n",
                        icalcomponent_get_summary (e_cal_component_get_icalcomponent (found->component)),
                        found->source_uid,
@@ -1351,7 +1380,8 @@ e_reminder_watcher_remove_from_snoozed (EReminderWatcher *watcher,
 
 static ECalClient *
 e_reminder_watcher_ref_client (EReminderWatcher *watcher,
-                              const gchar *source_uid)
+                              const gchar *source_uid,
+                              GCancellable *cancellable)
 {
        ECalClient *client = NULL;
        GSList *link;
@@ -1372,7 +1402,47 @@ e_reminder_watcher_ref_client (EReminderWatcher *watcher,
                }
        }
 
-       g_rec_mutex_unlock (&watcher->priv->lock);
+       if (!client && cancellable) {
+               ESourceRegistry *registry;
+               ESource *source;
+
+               registry = g_object_ref (watcher->priv->registry);
+
+               g_rec_mutex_unlock (&watcher->priv->lock);
+
+               source = e_source_registry_ref_source (registry, source_uid);
+               if (source) {
+                       ECalClientSourceType source_type = E_CAL_CLIENT_SOURCE_TYPE_LAST;
+
+                       if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR))
+                               source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS;
+                       else if (e_source_has_extension (source, E_SOURCE_EXTENSION_MEMO_LIST))
+                               source_type = E_CAL_CLIENT_SOURCE_TYPE_MEMOS;
+                       else if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST))
+                               source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS;
+
+                       if (source_type != E_CAL_CLIENT_SOURCE_TYPE_LAST) {
+                               EClient *tmp_client;
+                               GError *local_error = NULL;
+
+                               tmp_client = e_cal_client_connect_sync (source, source_type, 30, cancellable, 
&local_error);
+                               if (tmp_client)
+                                       client = E_CAL_CLIENT (tmp_client);
+
+                               if (!client) {
+                                       e_reminder_watcher_debug_print ("Failed to connect client '%s': 
%s\n", source_uid, local_error ? local_error->message : "Unknown error");
+                                       g_clear_error (&local_error);
+                               } else if (tmp_client) {
+                                       client = E_CAL_CLIENT (tmp_client);
+                               }
+                       }
+               }
+
+               g_clear_object (&source);
+               g_clear_object (&registry);
+       } else {
+               g_rec_mutex_unlock (&watcher->priv->lock);
+       }
 
        return client;
 }
@@ -1437,6 +1507,11 @@ e_reminder_watcher_maybe_schedule_next_trigger (EReminderWatcher *watcher,
 {
        g_rec_mutex_lock (&watcher->priv->lock);
 
+       if (!watcher->priv->timers_enabled) {
+               g_rec_mutex_unlock (&watcher->priv->lock);
+               return;
+       }
+
        e_reminder_watcher_calc_next_midnight (watcher);
 
        if (watcher->priv->snoozed && watcher->priv->snoozed->data) {
@@ -1629,7 +1704,9 @@ e_reminder_watcher_client_connect_cb (GObject *source_object,
                e_reminder_watcher_debug_print ("Connected client: %s (%s)\n", e_source_get_uid (source), 
e_source_get_display_name (source));
 
                watcher->priv->clients = g_slist_prepend (watcher->priv->clients, cd);
-               client_data_start_view (cd, watcher->priv->next_midnight, watcher->priv->cancellable);
+
+               if (watcher->priv->timers_enabled)
+                       client_data_start_view (cd, watcher->priv->next_midnight, watcher->priv->cancellable);
        }
 
        g_rec_mutex_unlock (&watcher->priv->lock);
@@ -1657,7 +1734,8 @@ e_reminder_watcher_source_appeared_cb (EReminderWatcher *watcher,
                return;
        }
 
-       e_cal_client_connect (source, source_type, 30, watcher->priv->cancellable, 
e_reminder_watcher_client_connect_cb, watcher);
+       if (watcher->priv->timers_enabled)
+               e_cal_client_connect (source, source_type, 30, watcher->priv->cancellable, 
e_reminder_watcher_client_connect_cb, watcher);
 
        g_rec_mutex_unlock (&watcher->priv->lock);
 }
@@ -1766,6 +1844,12 @@ e_reminder_watcher_set_property (GObject *object,
                                E_REMINDER_WATCHER (object),
                                g_value_get_boxed (value));
                        return;
+
+               case PROP_TIMERS_ENABLED:
+                       e_reminder_watcher_set_timers_enabled (
+                               E_REMINDER_WATCHER (object),
+                               g_value_get_boolean (value));
+                       return;
        }
 
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -1791,6 +1875,13 @@ e_reminder_watcher_get_property (GObject *object,
                                e_reminder_watcher_dup_default_zone (
                                E_REMINDER_WATCHER (object)));
                        return;
+
+               case PROP_TIMERS_ENABLED:
+                       g_value_set_boolean (
+                               value,
+                               e_reminder_watcher_get_timers_enabled (
+                               E_REMINDER_WATCHER (object)));
+                       return;
        }
 
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -1894,6 +1985,7 @@ e_reminder_watcher_class_init (EReminderWatcherClass *klass)
        object_class->finalize = e_reminder_watcher_finalize;
 
        klass->schedule_timer = e_reminder_watcher_schedule_timer_impl;
+       klass->format_time = e_reminder_watcher_format_time_impl;
 
        /**
         * EReminderWatcher:registry:
@@ -1934,47 +2026,76 @@ e_reminder_watcher_class_init (EReminderWatcherClass *klass)
                        G_PARAM_STATIC_STRINGS));
 
        /**
-        * EReminderWatcher::triggered:
+        * EReminderWatcher:timers-enabled:
+        *
+        * Whether timers are enabled for the #EReminderWatcher. See
+        * e_reminder_watcher_set_timers_enabled() for more information
+        * what it means.
+        *
+        * Default: %TRUE
+        *
+        * Since: 3.30
+        **/
+       g_object_class_install_property (
+               object_class,
+               PROP_TIMERS_ENABLED,
+               g_param_spec_boolean (
+                       "timers-enabled",
+                       "Timers Enabled",
+                       "Whether can schedule timers",
+                       TRUE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_STATIC_STRINGS));
+
+       /**
+        * EReminderWatcher::format-time:
         * @watcher: an #EReminderWatcher
-        * @reminders: (element-type EReminderData): a #GSList of #EReminderData
+        * @rd: an #EReminderData
+        * @itt: a pointer to struct icaltimetype
+        * @inout_buffer: (caller allocates) (inout): a pointer to a buffer to fill with formatted @itt
+        * @buffer_size: size of inout_buffer
         *
-        * Signal is emitted when any reminder is either overdue or triggered.
+        * Formats time @itt to a string and writes it to @inout_buffer, which can hold
+        * up to @buffer_size bytes. The first character of @inout_buffer is the nul-byte
+        * when nothing wrote to it yet.
         *
         * Since: 3.30
         **/
-       signals[TRIGGERED] = g_signal_new (
-               "triggered",
+       signals[FORMAT_TIME] = g_signal_new (
+               "format-time",
                G_OBJECT_CLASS_TYPE (klass),
-               G_SIGNAL_RUN_LAST,
-               G_STRUCT_OFFSET (EReminderWatcherClass, triggered),
+               G_SIGNAL_ACTION,
+               G_STRUCT_OFFSET (EReminderWatcherClass, format_time),
                NULL,
                NULL,
                g_cclosure_marshal_generic,
-               G_TYPE_NONE, 1,
-               G_TYPE_POINTER);
+               G_TYPE_NONE, 4,
+               G_TYPE_POINTER,
+               G_TYPE_POINTER,
+               G_TYPE_POINTER,
+               G_TYPE_INT);
 
        /**
-        * EReminderWatcher::removed:
+        * EReminderWatcher::triggered:
         * @watcher: an #EReminderWatcher
         * @reminders: (element-type EReminderData): a #GSList of #EReminderData
+        * @snoozed: %TRUE, when the @reminders had been snoozed, %FALSE otherwise
         *
-        * Signal is emitted when any reminder is removed from the past reminders.
-        * It's also followed by an EReminderWatcher::changed signal. This is used
-        * when it's known which reminders had been removed from the list of past
-        * reminders. It's not used when there's a notification from GSettings.
+        * Signal is emitted when any reminder is either overdue or triggered.
         *
         * Since: 3.30
         **/
-       signals[REMOVED] = g_signal_new (
-               "removed",
+       signals[TRIGGERED] = g_signal_new (
+               "triggered",
                G_OBJECT_CLASS_TYPE (klass),
                G_SIGNAL_RUN_LAST,
-               G_STRUCT_OFFSET (EReminderWatcherClass, removed),
+               G_STRUCT_OFFSET (EReminderWatcherClass, triggered),
                NULL,
                NULL,
                g_cclosure_marshal_generic,
-               G_TYPE_NONE, 1,
-               G_TYPE_POINTER);
+               G_TYPE_NONE, 2,
+               G_TYPE_POINTER,
+               G_TYPE_BOOLEAN);
 
        /**
         * EReminderWatcher::changed:
@@ -2001,11 +2122,24 @@ e_reminder_watcher_class_init (EReminderWatcherClass *klass)
 static void
 e_reminder_watcher_init (EReminderWatcher *watcher)
 {
+       icaltimezone *zone = NULL;
+       gchar *location;
+
+       location = e_cal_system_timezone_get_location ();
+       if (location) {
+               zone = icaltimezone_get_builtin_timezone (location);
+               g_free (location);
+       }
+
+       if (!zone)
+               zone = icaltimezone_get_utc_timezone ();
+
        watcher->priv = G_TYPE_INSTANCE_GET_PRIVATE (watcher, E_TYPE_REMINDER_WATCHER, 
EReminderWatcherPrivate);
        watcher->priv->cancellable = g_cancellable_new ();
        watcher->priv->settings = g_settings_new ("org.gnome.evolution-data-server.calendar");
        watcher->priv->scheduled = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 
e_reminder_watcher_free_rd_slist);
-       watcher->priv->default_zone = icaltimezone_get_utc_timezone ();
+       watcher->priv->default_zone = icaltimezone_copy (zone);
+       watcher->priv->timers_enabled = TRUE;
 
        g_rec_mutex_init (&watcher->priv->lock);
 }
@@ -2048,6 +2182,26 @@ e_reminder_watcher_get_registry (EReminderWatcher *watcher)
 }
 
 /**
+ * e_reminders_widget_ref_opened_client:
+ * @watcher: an #EReminderWatcher
+ * @source_uid: an #ESource UID of the calendar to return
+ *
+ * Returns: (nullable) (transfer full): a referenced #ECalClient for the @source_uid,
+ *    if any such is opened; %NULL otherwise.
+ *
+ * Since: 3.30
+ **/
+ECalClient *
+e_reminder_watcher_ref_opened_client (EReminderWatcher *watcher,
+                                     const gchar *source_uid)
+{
+       g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), NULL);
+       g_return_val_if_fail (source_uid != NULL, NULL);
+
+       return e_reminder_watcher_ref_client (watcher, source_uid, NULL);
+}
+
+/**
  * e_reminder_watcher_set_default_zone:
  * @watcher: an #EReminderWatcher
  * @zone: (nullable): an icaltimezone or #EReminderWatcherZone structure
@@ -2112,6 +2266,279 @@ e_reminder_watcher_dup_default_zone (EReminderWatcher *watcher)
        return zone;
 }
 
+/**
+ * e_reminder_watcher_get_timers_enabled:
+ * @watcher: an #EReminderWatcher
+ *
+ * Returns: whether timers are enabled for the @watcher. See
+ *    e_reminder_watcher_set_timers_enabled() for more information
+ *    what it means.
+ *
+ * Since: 3.30
+ **/
+gboolean
+e_reminder_watcher_get_timers_enabled (EReminderWatcher *watcher)
+{
+       gboolean enabled;
+
+       g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), FALSE);
+
+       g_rec_mutex_lock (&watcher->priv->lock);
+
+       enabled = watcher->priv->timers_enabled;
+
+       g_rec_mutex_unlock (&watcher->priv->lock);
+
+       return enabled;
+}
+
+/**
+ * e_reminder_watcher_set_timers_enabled:
+ * @watcher: an #EReminderWatcher
+ * @enable: a value to set
+ *
+ * The @watcher can be used both for scheduling the timers for the reminders
+ * and respond to them through the "triggered" signal, or only to listen for
+ * changes on the past reminders. The default is to have timers enabled, thus
+ * to response to scheduled reminders. Disabling the timers also means there
+ * will be less resources needed by the @watcher.
+ *
+ * Since: 3.30
+ **/
+void
+e_reminder_watcher_set_timers_enabled (EReminderWatcher *watcher,
+                                      gboolean enabled)
+{
+       g_return_if_fail (E_IS_REMINDER_WATCHER (watcher));
+
+       g_rec_mutex_lock (&watcher->priv->lock);
+
+       if (!enabled == !watcher->priv->timers_enabled) {
+               g_rec_mutex_unlock (&watcher->priv->lock);
+               return;
+       }
+
+       watcher->priv->timers_enabled = enabled;
+
+       if (watcher->priv->timers_enabled &&
+           !watcher->priv->construct_idle_id) {
+               e_source_registry_watcher_reclaim (watcher->priv->registry_watcher);
+               e_reminder_watcher_maybe_schedule_next_trigger (watcher, 0);
+       }
+
+       g_rec_mutex_unlock (&watcher->priv->lock);
+
+       g_object_notify (G_OBJECT (watcher), "timers-enabled");
+}
+
+static gchar *
+e_reminder_watcher_get_alarm_summary (EReminderWatcher *watcher,
+                                     const EReminderData *rd)
+{
+       ECalComponentText summary_text, alarm_text;
+       ECalComponentAlarm *alarm;
+       gchar *alarm_summary;
+
+       g_return_val_if_fail (watcher != NULL, NULL);
+       g_return_val_if_fail (rd != NULL, NULL);
+
+       summary_text.value = NULL;
+       alarm_text.value = NULL;
+
+       e_cal_component_get_summary (rd->component, &summary_text);
+
+       alarm = e_cal_component_get_alarm (rd->component, rd->instance.auid);
+       if (alarm) {
+               ECalClient *client;
+
+               client = e_reminder_watcher_ref_opened_client (watcher, rd->source_uid);
+
+               if (client && e_client_check_capability (E_CLIENT (client), 
CAL_STATIC_CAPABILITY_ALARM_DESCRIPTION)) {
+                       e_cal_component_alarm_get_description (alarm, &alarm_text);
+                       if (!alarm_text.value || !*alarm_text.value)
+                               alarm_text.value = NULL;
+               }
+
+               g_clear_object (&client);
+       }
+
+       if (alarm_text.value && summary_text.value &&
+           e_util_utf8_strcasecmp (alarm_text.value, summary_text.value) == 0)
+               alarm_text.value = NULL;
+
+       if (summary_text.value && *summary_text.value &&
+           alarm_text.value && *alarm_text.value)
+               alarm_summary = g_strconcat (summary_text.value, "\n", alarm_text.value, NULL);
+       else if (summary_text.value && *summary_text.value)
+               alarm_summary = g_strdup (summary_text.value);
+       else if (alarm_text.value && *alarm_text.value)
+               alarm_summary = g_strdup (alarm_text.value);
+       else
+               alarm_summary = NULL;
+
+       if (alarm)
+               e_cal_component_alarm_free (alarm);
+
+       return alarm_summary;
+}
+
+/**
+ * e_reminder_watcher_describe_data:
+ * @watcher: an #EReminderWatcher
+ * @rd: an #EReminderData
+ * @flags: bit-or of #EReminderWatcherDescribeFlags
+ *
+ * Returns a new string with a text description of the @rd. The text format
+ * can be influenced with @flags.
+ *
+ * Free the returned string with g_free(), when no longer needed.
+ *
+ * Returns: (transfer full): a new string with a text description of the @rd.
+ *
+ * Since: 3.30
+ **/
+gchar *
+e_reminder_watcher_describe_data (EReminderWatcher *watcher,
+                                 const EReminderData *rd,
+                                 guint32 flags)
+{
+       icalcomponent *icalcomp;
+       gchar *description = NULL;
+       gboolean use_markup;
+
+       g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), NULL);
+       g_return_val_if_fail (rd != NULL, NULL);
+
+       use_markup = (flags & E_REMINDER_WATCHER_DESCRIBE_FLAG_MARKUP) != 0;
+
+       icalcomp = e_cal_component_get_icalcomponent (rd->component);
+       if (icalcomp) {
+               gchar *summary;
+               const gchar *location;
+               gchar *timediff = NULL, *tmp;
+               gchar timestr[255];
+               GString *markup;
+
+               timestr[0] = 0;
+               markup = g_string_sized_new (256);
+               summary = e_reminder_watcher_get_alarm_summary (watcher, rd);
+               location = icalcomponent_get_location (icalcomp);
+
+               if (rd->instance.occur_start > 0) {
+                       gchar *timestrptr = timestr;
+                       icaltimezone *zone;
+                       struct icaltimetype itt;
+                       gboolean is_date = FALSE;
+
+                       if (rd->instance.occur_end > rd->instance.occur_start) {
+                               timediff = e_cal_util_seconds_to_string (rd->instance.occur_end - 
rd->instance.occur_start);
+                       }
+
+                       zone = e_reminder_watcher_dup_default_zone (watcher);
+                       if (zone && (!icaltimezone_get_location (zone) || g_strcmp0 
(icaltimezone_get_location (zone), "UTC") == 0)) {
+                               icaltimezone_free (zone, 1);
+                               zone = NULL;
+                       }
+
+                       itt = icalcomponent_get_dtstart (icalcomp);
+                       if (icaltime_is_valid_time (itt) && !icaltime_is_null_time (itt))
+                               is_date = itt.is_date;
+
+                       itt = icaltime_from_timet_with_zone (rd->instance.occur_start, is_date, zone);
+
+                       g_signal_emit (watcher, signals[FORMAT_TIME], 0, rd, &itt, &timestrptr, 254, NULL);
+
+                       if (!*timestr)
+                               e_reminder_watcher_format_time_impl (watcher, rd, &itt, &timestrptr, 254);
+
+                       if (zone)
+                               icaltimezone_free (zone, 1);
+               }
+
+               if (!summary || !*summary) {
+                       g_free (summary);
+                       summary = g_strdup (_( "No Summary"));
+               }
+
+               if (use_markup) {
+                       tmp = g_markup_printf_escaped ("<b>%s</b>", summary);
+                       g_string_append (markup, tmp);
+                       g_free (tmp);
+               } else {
+                       g_string_append (markup, summary);
+               }
+               g_string_append_c (markup, '\n');
+
+               if (*timestr) {
+                       /* Translators: The first %s is replaced with the time string,
+                          the second %s with a duration, and the third %s with an event location,
+                          making it something like: "24.1.2018 10:30 (30 minutes) Meeting room A1" */
+                       #define FMT_TIME_TIME_LOCATION C_("overdue", "%s (%s) %s")
+
+                       /* Translators: The first %s is replaced with the time string,
+                          the second %s with a duration, making is something like:
+                          "24.1.2018 10:30 (30 minutes)" */
+                       #define FMT_TIME_TIME C_("overdue", "%s (%s)")
+
+                       /* Translators: The first %s is replaced with the time string,
+                          the second %s with an event location, making it something like:
+                          "24.1.2018 10:30 Meeting room A1" */
+                       #define FMT_TIME_LOCATION C_("overdue", "%s %s")
+
+                       if (timediff && *timediff) {
+                               if (location && *location) {
+                                       if (use_markup)
+                                               tmp = g_markup_printf_escaped (FMT_TIME_TIME_LOCATION, 
timestr, timediff, location);
+                                       else
+                                               tmp = g_strdup_printf (FMT_TIME_TIME_LOCATION, timestr, 
timediff, location);
+                               } else {
+                                       if (use_markup)
+                                               tmp = g_markup_printf_escaped (FMT_TIME_TIME, timestr, 
timediff);
+                                       else
+                                               tmp = g_strdup_printf (FMT_TIME_TIME, timestr, timediff);
+                               }
+                       } else if (location && *location) {
+                               if (use_markup)
+                                       tmp = g_markup_printf_escaped (FMT_TIME_LOCATION, timestr, location);
+                               else
+                                       tmp = g_strdup_printf (FMT_TIME_LOCATION, timestr, location);
+                       } else {
+                               if (use_markup)
+                                       tmp = g_markup_escape_text (timestr, -1);
+                               else
+                                       tmp = g_strdup (timestr);
+                       }
+
+                       if (use_markup)
+                               g_string_append (markup, "<small>");
+                       g_string_append (markup, tmp);
+                       if (use_markup)
+                               g_string_append (markup, "</small>");
+
+                       g_free (tmp);
+               } else if (location && *location) {
+                       if (use_markup) {
+                               tmp = g_markup_printf_escaped ("%s", location);
+
+                               g_string_append (markup, "<small>");
+                               g_string_append (markup, tmp);
+                               g_string_append (markup, "</small>");
+
+                               g_free (tmp);
+                       } else {
+                               g_string_append (markup, location);
+                       }
+               }
+
+               description = g_string_free (markup, FALSE);
+
+               g_free (timediff);
+               g_free (summary);
+       }
+
+       return description;
+}
+
 typedef struct _ForeachTriggerData {
        gint64 current_time;
        GSList *triggered; /* EReminderData * */
@@ -2183,7 +2610,7 @@ void
 e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
 {
        ForeachTriggerData ftd;
-       GSList *snoozed, *link;
+       GSList *snoozed, *link, *triggered_snoozed = NULL;
        gboolean changed = FALSE;
 
        g_return_if_fail (E_IS_REMINDER_WATCHER (watcher));
@@ -2230,13 +2657,13 @@ e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
 
                        changed = e_reminder_watcher_remove_from_snoozed (watcher, rd, FALSE) || changed;
 
-                       ftd.triggered = g_slist_prepend (ftd.triggered, rd);
+                       triggered_snoozed = g_slist_prepend (triggered_snoozed, rd);
                }
        }
 
        g_slist_free_full (snoozed, e_reminder_data_free);
 
-       if (ftd.triggered) {
+       if (ftd.triggered || triggered_snoozed) {
                GHashTable *last_notifies;
                GHashTableIter iter;
                GSList *past;
@@ -2266,6 +2693,26 @@ e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
                        }
                }
 
+               for (link = triggered_snoozed; link; link = g_slist_next (link)) {
+                       EReminderData *rd = e_reminder_data_copy (link->data);
+
+                       if (rd) {
+                               if (e_reminder_watcher_add (&past, rd, TRUE, FALSE)) {
+                                       time_t *ptrigger;
+
+                                       ptrigger = g_hash_table_lookup (last_notifies, rd->source_uid);
+                                       if (ptrigger) {
+                                               if (*ptrigger < rd->instance.trigger)
+                                                       *ptrigger = rd->instance.trigger;
+                                       } else {
+                                               ptrigger = g_new0 (time_t, 1);
+                                               *ptrigger = rd->instance.trigger;
+                                               g_hash_table_insert (last_notifies, rd->source_uid, ptrigger);
+                                       }
+                               }
+                       }
+               }
+
                e_reminder_watcher_save_past (watcher, past);
 
                g_hash_table_iter_init (&iter, last_notifies);
@@ -2274,7 +2721,7 @@ e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
                        const time_t *ptrigger = value;
 
                        if (source_uid && ptrigger) {
-                               ECalClient *client = e_reminder_watcher_ref_client (watcher, source_uid);
+                               ECalClient *client = e_reminder_watcher_ref_client (watcher, source_uid, 
NULL);
 
                                if (client) {
                                        client_set_last_notification_time (client, *ptrigger);
@@ -2291,10 +2738,16 @@ e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
        if (changed)
                e_reminder_watcher_save_snoozed (watcher);
 
-       if (ftd.triggered) {
-               e_reminder_watcher_emit_signal_idle_multiple (watcher, signals[TRIGGERED], ftd.triggered);
+       if (ftd.triggered || triggered_snoozed) {
+               if (triggered_snoozed)
+                       e_reminder_watcher_emit_signal_idle_multiple (watcher, signals[TRIGGERED], 
triggered_snoozed, TRUE);
+
+               if (ftd.triggered)
+                       e_reminder_watcher_emit_signal_idle_multiple (watcher, signals[TRIGGERED], 
ftd.triggered, FALSE);
+
                e_reminder_watcher_emit_signal_idle (watcher, signals[CHANGED], NULL);
 
+               g_slist_free_full (triggered_snoozed, e_reminder_data_free);
                g_slist_free_full (ftd.triggered, e_reminder_data_free);
        }
 
@@ -2375,9 +2828,8 @@ e_reminder_watcher_dup_snoozed (EReminderWatcher *watcher)
  *
  * Snoozes @rd until @until, which is an absolute time when the @rd
  * should be retriggered. This moves the @rd from the list of past
- * reminders into the list of snoozed reminders and invokes the "removed"
- * signal when the @rd was in the past reminders. It also invokes
- * the "changed" signal.
+ * reminders into the list of snoozed reminders and invokes the "changed"
+ * signal.
  *
  * Since: 3.30
  **/
@@ -2522,9 +2974,9 @@ e_reminder_watcher_dismiss_one_sync (ECalClient *client,
 
                success = e_cal_client_discard_alarm_sync (client, id->uid, id->rid, rd->instance.auid, 
cancellable, &local_error);
 
-               e_reminder_watcher_debug_print ("Discard alarm for '%s' from %s %s%s%s%s\n",
+               e_reminder_watcher_debug_print ("Discard alarm for '%s' from %s (uid:%s rid:%s auid:%s) 
%s%s%s%s\n",
                        icalcomponent_get_summary (e_cal_component_get_icalcomponent (rd->component)),
-                       rd->source_uid,
+                       rd->source_uid, id->uid, id->rid ? id->rid : "null", rd->instance.auid,
                        success ? "succeeded" : "failed",
                        (!success || local_error) ? " (" : "",
                        local_error ? local_error->message : success ? "" : "Unknown error",
@@ -2583,7 +3035,7 @@ e_reminder_watcher_dismiss_sync (EReminderWatcher *watcher,
        changed = e_reminder_watcher_remove_from_snoozed (watcher, rd_copy, TRUE) || changed;
 
        if (changed)
-               client = e_reminder_watcher_ref_client (watcher, rd_copy->source_uid);
+               client = e_reminder_watcher_ref_client (watcher, rd_copy->source_uid, cancellable ? 
cancellable : watcher->priv->cancellable);
 
        e_reminder_watcher_maybe_schedule_next_trigger (watcher, 0);
 
@@ -2699,36 +3151,36 @@ e_reminder_watcher_dismiss_all_sync (EReminderWatcher *watcher,
                                     GCancellable *cancellable,
                                     GError **error)
 {
-       GSList *reminders, *link, *dismissed = NULL;
+       GHashTable *clients; /* gchar *source_uid ~> ECalClient * */
+       GSList *reminders, *link;
        gboolean success = TRUE, changed = FALSE;
 
        g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), FALSE);
 
        g_rec_mutex_lock (&watcher->priv->lock);
 
+       clients = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
        reminders = e_reminder_watcher_dup_past (watcher);
 
        for (link = reminders; link; link = g_slist_next (link)) {
                EReminderData *rd = link->data;
                ECalClient *client;
 
-               client = e_reminder_watcher_ref_client (watcher, rd->source_uid);
+               client = g_hash_table_lookup (clients, rd->source_uid);
+               if (!client) {
+                       client = e_reminder_watcher_ref_client (watcher, rd->source_uid, cancellable ? 
cancellable : watcher->priv->cancellable);
+                       if (client) {
+                               g_hash_table_insert (clients, g_strdup (rd->source_uid), client);
+                       }
+               }
+
                if (client) {
                        success = e_reminder_watcher_dismiss_one_sync (client, rd, cancellable, error);
-                       g_object_unref (client);
 
                        /* To keep the failed discard in the saved list. */
                        if (!success)
                                break;
                }
-
-               dismissed = g_slist_prepend (dismissed, rd);
-               link->data = NULL;
-       }
-
-       if (dismissed) {
-               e_reminder_watcher_emit_signal_idle_multiple (watcher, signals[REMOVED], dismissed);
-               g_slist_free_full (dismissed, e_reminder_data_free);
        }
 
        if (link != reminders && reminders) {
@@ -2737,6 +3189,7 @@ e_reminder_watcher_dismiss_all_sync (EReminderWatcher *watcher,
        }
 
        g_slist_free_full (reminders, e_reminder_data_free);
+       g_hash_table_destroy (clients);
 
        g_rec_mutex_unlock (&watcher->priv->lock);
 
diff --git a/src/calendar/libecal/e-reminder-watcher.h b/src/calendar/libecal/e-reminder-watcher.h
index 96a830e..3cb6a33 100644
--- a/src/calendar/libecal/e-reminder-watcher.h
+++ b/src/calendar/libecal/e-reminder-watcher.h
@@ -23,6 +23,7 @@
 #define E_REMINDER_WATCHER_H
 
 #include <libedataserver/libedataserver.h>
+#include <libecal/e-cal-client.h>
 
 /* Standard GObject macros */
 #define E_TYPE_REMINDER_WATCHER \
@@ -71,7 +72,7 @@ void          e_reminder_data_free            (gpointer rd); /* EReminderData * */
 /**
  * EReminderWatcherZone:
  *
- * A libical's icaltimezone encapsulated as a GByxed type.
+ * A libical's icaltimezone encapsulated as a GBoxed type.
  * It can be retyped into icaltimezone directly.
  *
  * Since: 3.30
@@ -88,6 +89,21 @@ typedef struct _EReminderWatcherClass EReminderWatcherClass;
 typedef struct _EReminderWatcherPrivate EReminderWatcherPrivate;
 
 /**
+ * EReminderWatcherDescribeFlags:
+ * @E_REMINDER_WATCHER_DESCRIBE_FLAG_NONE: None flags
+ * @E_REMINDER_WATCHER_DESCRIBE_FLAG_MARKUP: Returned description will contain
+ *    also markup. Without it it'll be a plain text.
+ *
+ * Flags modifying behaviour of e_reminder_watcher_describe_data().
+ *
+ * Since: 3.30
+ **/
+typedef enum { /*< flags >*/
+       E_REMINDER_WATCHER_DESCRIBE_FLAG_NONE   = 0,
+       E_REMINDER_WATCHER_DESCRIBE_FLAG_MARKUP = (1 << 1)
+} EReminderWatcherDescribeFlags;
+
+/**
  * EReminderWatcher:
  *
  * Contains only private data that should be read and manipulated using the
@@ -107,10 +123,14 @@ struct _EReminderWatcherClass {
        /* Virtual methods and signals */
        void            (* schedule_timer)      (EReminderWatcher *watcher,
                                                 gint64 at_time);
+       void            (* format_time)         (EReminderWatcher *watcher,
+                                                const EReminderData *rd,
+                                                struct icaltimetype *itt,
+                                                gchar **inout_buffer,
+                                                gint buffer_size);
        void            (* triggered)           (EReminderWatcher *watcher,
-                                                const GSList *reminders); /* EReminderData * */
-       void            (* removed)             (EReminderWatcher *watcher,
-                                                const GSList *reminders); /* EReminderData * */
+                                                const GSList *reminders, /* EReminderData * */
+                                                gboolean snoozed);
        void            (* changed)             (EReminderWatcher *watcher);
 
        /* Padding for future expansion */
@@ -122,9 +142,17 @@ EReminderWatcher *
                e_reminder_watcher_new                  (ESourceRegistry *registry);
 ESourceRegistry *
                e_reminder_watcher_get_registry         (EReminderWatcher *watcher);
+ECalClient *   e_reminder_watcher_ref_opened_client    (EReminderWatcher *watcher,
+                                                        const gchar *source_uid);
 void           e_reminder_watcher_set_default_zone     (EReminderWatcher *watcher,
                                                         const icaltimezone *zone);
 icaltimezone * e_reminder_watcher_dup_default_zone     (EReminderWatcher *watcher);
+gboolean       e_reminder_watcher_get_timers_enabled   (EReminderWatcher *watcher);
+void           e_reminder_watcher_set_timers_enabled   (EReminderWatcher *watcher,
+                                                        gboolean enabled);
+gchar *                e_reminder_watcher_describe_data        (EReminderWatcher *watcher,
+                                                        const EReminderData *rd,
+                                                        guint32 flags); /* bit-or of 
EReminderWatcherDescribeFlags */
 void           e_reminder_watcher_timer_elapsed        (EReminderWatcher *watcher);
 GSList *       e_reminder_watcher_dup_past             (EReminderWatcher *watcher); /* EReminderData * */
 GSList *       e_reminder_watcher_dup_snoozed          (EReminderWatcher *watcher); /* EReminderData * */
diff --git a/src/libedataserver/CMakeLists.txt b/src/libedataserver/CMakeLists.txt
index f4e7caa..e82b3d7 100644
--- a/src/libedataserver/CMakeLists.txt
+++ b/src/libedataserver/CMakeLists.txt
@@ -244,6 +244,7 @@ target_compile_definitions(edataserver PRIVATE
        -DE_DATA_SERVER_LOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
        -DE_DATA_SERVER_IMAGESDIR=\"${imagesdir}\"
        -DE_DATA_SERVER_CREDENTIALMODULEDIR=\"${credentialmoduledir}\"
+       -DE_DATA_SERVER_UIMODULEDIR=\"${uimoduledir}\"
        -DE_DATA_SERVER_PRIVDATADIR=\"${privdatadir}\"
        -DLIBEDATASERVER_COMPILATION
 )
diff --git a/src/libedataserver/e-data-server-util.c b/src/libedataserver/e-data-server-util.c
index 5f2c76b..db50c34 100644
--- a/src/libedataserver/e-data-server-util.c
+++ b/src/libedataserver/e-data-server-util.c
@@ -1863,6 +1863,7 @@ static const gchar *cp_prefix;
 static const gchar *localedir;
 static const gchar *imagesdir;
 static const gchar *credentialmoduledir;
+static const gchar *uimoduledir;
 
 static HMODULE hmodule;
 G_LOCK_DEFINE_STATIC (mutex);
@@ -1971,6 +1972,7 @@ setup (void)
        localedir = replace_prefix (cp_prefix, E_DATA_SERVER_LOCALEDIR);
        imagesdir = replace_prefix (prefix, E_DATA_SERVER_IMAGESDIR);
        credentialmoduledir = replace_prefix (prefix, E_DATA_SERVER_CREDENTIALMODULEDIR);
+       uimoduledir = replace_prefix (prefix, E_DATA_SERVER_UIMODULEDIR);
 
        G_UNLOCK (mutex);
 }
@@ -1995,6 +1997,7 @@ e_util_get_##varbl (void) \
 
 PRIVATE_GETTER (imagesdir)
 PRIVATE_GETTER (credentialmoduledir);
+PRIVATE_GETTER (uimoduledir);
 
 PUBLIC_GETTER (prefix)
 PUBLIC_GETTER (cp_prefix)
diff --git a/src/libedataserver/libedataserver-private.h b/src/libedataserver/libedataserver-private.h
index b9733f0..abba7b2 100644
--- a/src/libedataserver/libedataserver-private.h
+++ b/src/libedataserver/libedataserver-private.h
@@ -25,6 +25,7 @@
 
 const gchar *  _libedataserver_get_imagesdir           (void) G_GNUC_CONST;
 const gchar *  _libedataserver_get_credentialmoduledir (void) G_GNUC_CONST;
+const gchar *  _libedataserver_get_uimoduledir         (void) G_GNUC_CONST;
 
 #undef E_DATA_SERVER_IMAGESDIR
 #define E_DATA_SERVER_IMAGESDIR _libedataserver_get_imagesdir ()
@@ -32,6 +33,9 @@ const gchar * _libedataserver_get_credentialmoduledir (void) G_GNUC_CONST;
 #undef E_DATA_SERVER_CREDENTIALMODULEDIR
 #define E_DATA_SERVER_CREDENTIALMODULEDIR _libedataserver_get_credentialmoduledir ()
 
+#undef E_DATA_SERVER_UIMODULEDIR
+#define E_DATA_SERVER_UIMODULEDIR _libedataserver_get_uimoduledir ()
+
 #endif /* G_OS_WIN32 */
 
 #endif /* LIBEDATASERVER_PRIVATE_H */
diff --git a/src/libedataserverui/CMakeLists.txt b/src/libedataserverui/CMakeLists.txt
index f039ff0..eebb7ec 100644
--- a/src/libedataserverui/CMakeLists.txt
+++ b/src/libedataserverui/CMakeLists.txt
@@ -6,8 +6,11 @@ set(SOURCES
        e-credentials-prompter-impl.c
        e-credentials-prompter-impl-oauth2.c
        e-credentials-prompter-impl-password.c
+       e-reminders-widget.c
        e-trust-prompt.c
        e-webdav-discover-widget.c
+       libedataserverui-private.h
+       libedataserverui-private.c
 )
 
 set(HEADERS
@@ -17,6 +20,7 @@ set(HEADERS
        e-credentials-prompter-impl.h
        e-credentials-prompter-impl-oauth2.h
        e-credentials-prompter-impl-password.h
+       e-reminders-widget.h
        e-trust-prompt.h
        e-webdav-discover-widget.h
 )
@@ -24,6 +28,7 @@ set(HEADERS
 set(DEPENDENCIES
        camel
        ebackend
+       ecal
        edataserver
 )
 
@@ -45,6 +50,7 @@ set_target_properties(edataserverui PROPERTIES
 target_compile_definitions(edataserverui PRIVATE
        -DG_LOG_DOMAIN=\"e-data-server-ui\"
        -DLIBEDATASERVERUI_COMPILATION
+       -DE_DATA_SERVER_UIMODULEDIR=\"${uimoduledir}\"
 )
 
 target_compile_options(edataserverui PUBLIC
@@ -104,14 +110,18 @@ set(gir_identifies_prefixes E)
 set(gir_includes GObject-2.0 Gio-2.0 Gtk-3.0 Soup-2.4)
 set(gir_cflags
        -DLIBEDATASERVERUI_COMPILATION
+       -I${CMAKE_BINARY_DIR}/src/calendar
+       -I${CMAKE_SOURCE_DIR}/src/calendar
 )
 set(gir_libdirs
        ${CMAKE_BINARY_DIR}/src/private
+       ${CMAKE_BINARY_DIR}/src/calendar/libecal
        ${CMAKE_BINARY_DIR}/src/camel
        ${CMAKE_BINARY_DIR}/src/libedataserver
 )
 set(gir_libs
        camel
+       ecal
        edataserver
        edataserverui
 )
diff --git a/src/libedataserverui/e-reminders-widget.c b/src/libedataserverui/e-reminders-widget.c
new file mode 100644
index 0000000..7b42c2d
--- /dev/null
+++ b/src/libedataserverui/e-reminders-widget.c
@@ -0,0 +1,1813 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION: e-reminders-widget
+ * @include: libedataserverui/libedataserverui.h
+ * @short_description: An #ERemindersWidget to work with past reminders
+ *
+ * The #ERemindersWidget is a widget which does common tasks on past reminders
+ * provided by #EReminderWatcher. The owner should connect to the "changed" signal
+ * to be notified on any changes, including when the list of past reminders
+ * is either expanded or shrunk, which usually causes the dialog with this
+ * widget to be shown or hidden.
+ *
+ * The widget itself is an #EExtensible.
+ *
+ * The widget does not listen to #EReminderWatcher::triggered signal.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "libedataserver/libedataserver.h"
+#include "libecal/libecal.h"
+
+#include "libedataserverui-private.h"
+
+#include "e-reminders-widget.h"
+
+#define MAX_CUSTOM_SNOOZE_VALUES 7
+
+struct _ERemindersWidgetPrivate {
+       EReminderWatcher *watcher;
+       GSettings *settings;
+       gboolean is_empty;
+
+       GtkTreeView *tree_view;
+       GtkWidget *dismiss_button;
+       GtkWidget *dismiss_all_button;
+       GtkWidget *snooze_combo;
+       GtkWidget *snooze_button;
+
+       GtkWidget *add_snooze_popover;
+       GtkWidget *add_snooze_days_spin;
+       GtkWidget *add_snooze_hours_spin;
+       GtkWidget *add_snooze_minutes_spin;
+       GtkWidget *add_snooze_add_button;
+
+       GtkInfoBar *info_bar;
+
+       GCancellable *cancellable;
+       guint refresh_idle_id;
+
+       gboolean is_mapped;
+       guint overdue_update_id;
+       gint64 last_overdue_update; /* in seconds */
+       gboolean overdue_update_rounded;
+
+       gboolean updating_snooze_combo;
+       gint last_selected_snooze_minutes; /* not the same as the saved value in GSettings */
+};
+
+enum {
+       CHANGED,
+       ACTIVATED,
+       LAST_SIGNAL
+};
+
+enum {
+       PROP_0,
+       PROP_WATCHER,
+       PROP_EMPTY
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE_WITH_CODE (ERemindersWidget, e_reminders_widget, GTK_TYPE_GRID,
+                        G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL))
+
+static gboolean
+reminders_widget_snooze_combo_separator_cb (GtkTreeModel *model,
+                                           GtkTreeIter *iter,
+                                           gpointer user_data)
+{
+       gint32 minutes = -1;
+
+       if (!model || !iter)
+               return FALSE;
+
+       gtk_tree_model_get (model, iter, 1, &minutes, -1);
+
+       return !minutes;
+}
+
+static GtkWidget *
+reminders_widget_new_snooze_combo (void)
+{
+       GtkWidget *combo;
+       GtkListStore *list_store;
+       GtkCellRenderer *renderer;
+
+       list_store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT);
+
+       combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (list_store));
+
+       g_object_unref (list_store);
+
+       renderer = gtk_cell_renderer_text_new ();
+       gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE);
+       gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer, "text", 0, NULL);
+
+       gtk_combo_box_set_row_separator_func (GTK_COMBO_BOX (combo),
+               reminders_widget_snooze_combo_separator_cb, NULL, NULL);
+
+       return combo;
+}
+
+static void
+reminders_widget_fill_snooze_combo (ERemindersWidget *reminders,
+                                   gint preselect_minutes)
+{
+       const gint predefined_minutes[] = {
+               5,
+               10,
+               15,
+               30,
+               60,
+               24 * 60,
+               7 * 24 * 60
+       };
+       gint ii, last_sel = -1;
+       GtkComboBox *combo;
+       GtkListStore *list_store;
+       GtkTreeIter iter, tosel_iter;
+       GVariant *variant;
+       gboolean tosel_set = FALSE;
+       gboolean any_stored_added = FALSE;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       reminders->priv->updating_snooze_combo = TRUE;
+
+       combo = GTK_COMBO_BOX (reminders->priv->snooze_combo);
+       list_store = GTK_LIST_STORE (gtk_combo_box_get_model (combo));
+
+       if (gtk_combo_box_get_active_iter (combo, &iter)) {
+               gtk_tree_model_get (GTK_TREE_MODEL (list_store), &iter, 1, &last_sel, -1);
+       }
+
+       gtk_list_store_clear (list_store);
+
+       #define add_minutes(_minutes) G_STMT_START {                            \
+               gint32 minutes = (_minutes);                                    \
+               gchar *text;                                                    \
+                                                                               \
+               text = e_cal_util_seconds_to_string (minutes * 60);             \
+               gtk_list_store_append (list_store, &iter);                      \
+               gtk_list_store_set (list_store, &iter,                          \
+                       0, text,                                                \
+                       1, minutes,                                             \
+                       -1);                                                    \
+               g_free (text);                                                  \
+                                                                               \
+               if (preselect_minutes > 0 && preselect_minutes == minutes) {    \
+                       tosel_set = TRUE;                                       \
+                       tosel_iter = iter;                                      \
+                       last_sel = -1;                                          \
+               } else if (last_sel > 0 && minutes == last_sel) {               \
+                       tosel_set = TRUE;                                       \
+                       tosel_iter = iter;                                      \
+               }                                                               \
+       } G_STMT_END
+
+       /* Custom user values first */
+       variant = g_settings_get_value (reminders->priv->settings, "notify-custom-snooze-minutes");
+       if (variant) {
+               const gint32 *stored;
+               gsize nstored = 0;
+
+               stored = g_variant_get_fixed_array (variant, &nstored, sizeof (gint32));
+               if (stored && nstored > 0) {
+                       for (ii = 0; ii < nstored; ii++) {
+                               if (stored[ii] > 0) {
+                                       add_minutes (stored[ii]);
+                                       any_stored_added = TRUE;
+                               }
+                       }
+               }
+
+               g_variant_unref (variant);
+
+               if (any_stored_added) {
+                       /* Separator */
+                       gtk_list_store_append (list_store, &iter);
+                       gtk_list_store_set (list_store, &iter, 1, 0, -1);
+               }
+       }
+
+       for (ii = 0; ii < G_N_ELEMENTS (predefined_minutes); ii++) {
+               add_minutes (predefined_minutes[ii]);
+       }
+
+       #undef add_minutes
+
+       /* Separator */
+       gtk_list_store_append (list_store, &iter);
+       gtk_list_store_set (list_store, &iter, 1, 0, -1);
+
+       gtk_list_store_append (list_store, &iter);
+       gtk_list_store_set (list_store, &iter, 0, _("Add custom time…"), 1, -1, -1);
+
+       if (any_stored_added) {
+               gtk_list_store_append (list_store, &iter);
+               gtk_list_store_set (list_store, &iter, 0, _("Clear custom times"), 1, -2, -1);
+       }
+
+       reminders->priv->updating_snooze_combo = FALSE;
+
+       if (tosel_set)
+               gtk_combo_box_set_active_iter (combo, &tosel_iter);
+       else
+               gtk_combo_box_set_active (combo, 0);
+}
+
+static void
+reminders_widget_custom_snooze_minutes_changed_cb (GSettings *settings,
+                                                  const gchar *key,
+                                                  gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       reminders_widget_fill_snooze_combo (reminders, -1);
+}
+
+static void
+reminders_get_reminder_markups (ERemindersWidget *reminders,
+                               const EReminderData *rd,
+                               gchar **out_overdue_markup,
+                               gchar **out_description_markup)
+{
+       g_return_if_fail (rd != NULL);
+
+       if (out_overdue_markup) {
+               gint64 diff;
+               gboolean in_future;
+               gchar *time_str;
+
+               diff = (g_get_real_time () / G_USEC_PER_SEC) - ((gint64) rd->instance.occur_start);
+               in_future = diff < 0;
+               if (in_future)
+                       diff = (-1) * diff;
+
+               /* in minutes */
+               if (in_future && (diff % 60) > 0)
+                       diff += 60;
+
+               diff = diff / 60;
+
+               if (!diff) {
+                       time_str = g_strdup (C_("overdue", "now"));
+               } else if (diff < 60) {
+                       time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d minute", "%d minutes", 
diff), (gint) diff);
+               } else if (diff < 24 * 60) {
+                       gint hours = diff / 60;
+
+                       time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d hour", "%d hours", 
hours), hours);
+               } else if (diff < 7 * 24 * 60) {
+                       gint days = diff / (24 * 60);
+
+                       time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d day", "%d days", days), 
days);
+               } else if (diff < 54 * 7 * 24 * 60) {
+                       gint weeks = diff / (7 * 24 * 60);
+
+                       time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d week", "%d weeks", 
weeks), weeks);
+               } else {
+                       gint years = diff / (366 * 24 * 60);
+
+                       time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d year", "%d years", 
years), years);
+               }
+
+               if (in_future || !diff) {
+                       *out_overdue_markup = g_markup_printf_escaped ("<span size=\"x-small\">%s</span>", 
time_str);
+               } else {
+                       *out_overdue_markup = g_markup_printf_escaped ("<span 
size=\"x-small\">%s\n%s</span>", time_str, C_("overdue", "overdue"));
+               }
+
+               g_free (time_str);
+       }
+
+       if (out_description_markup) {
+               *out_description_markup = e_reminder_watcher_describe_data (reminders->priv->watcher, rd, 
E_REMINDER_WATCHER_DESCRIBE_FLAG_MARKUP);
+       }
+}
+
+static void
+reminders_widget_overdue_update (ERemindersWidget *reminders)
+{
+       GtkListStore *list_store;
+       GtkTreeModel *model;
+       GtkTreeIter iter;
+       gboolean any_changed = FALSE;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       model = gtk_tree_view_get_model (reminders->priv->tree_view);
+       if (!model)
+               return;
+
+       if (!gtk_tree_model_get_iter_first (model, &iter))
+               return;
+
+       list_store = GTK_LIST_STORE (model);
+
+       do {
+               EReminderData *rd = NULL;
+
+               gtk_tree_model_get (model, &iter,
+                       E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA, &rd,
+                       -1);
+
+               if (rd) {
+                       gchar *overdue_markup = NULL;
+
+                       reminders_get_reminder_markups (reminders, rd, &overdue_markup, NULL);
+                       if (overdue_markup) {
+                               gchar *current = NULL;
+
+                               gtk_tree_model_get (model, &iter,
+                                       E_REMINDERS_WIDGET_COLUMN_OVERDUE, &current,
+                                       -1);
+
+                               if (g_strcmp0 (current, overdue_markup) != 0) {
+                                       gtk_list_store_set (list_store, &iter,
+                                               E_REMINDERS_WIDGET_COLUMN_OVERDUE, overdue_markup,
+                                               -1);
+                                       any_changed = TRUE;
+                               }
+
+                               g_free (overdue_markup);
+                               g_free (current);
+                       }
+
+                       e_reminder_data_free (rd);
+               }
+       } while (gtk_tree_model_iter_next (model, &iter));
+
+       if (any_changed) {
+               GtkTreeViewColumn *column;
+
+               column = gtk_tree_view_get_column (reminders->priv->tree_view, 0);
+               if (column)
+                       gtk_tree_view_column_queue_resize (column);
+       }
+}
+
+static gboolean
+reminders_widget_overdue_update_cb (gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+       gint64 now_seconds, last_update;
+
+       if (g_source_is_destroyed (g_main_current_source ()))
+               return FALSE;
+
+       g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), FALSE);
+
+       reminders_widget_overdue_update (reminders);
+
+       now_seconds = g_get_real_time () / G_USEC_PER_SEC;
+       last_update = reminders->priv->last_overdue_update;
+       reminders->priv->last_overdue_update = now_seconds;
+
+       if (!last_update || (
+           (now_seconds - last_update) % 60 > 2 &&
+           (now_seconds - last_update) % 60 < 58)) {
+               gint until_minute = 60 - (now_seconds % 60);
+
+               if (until_minute >= 59) {
+                       reminders->priv->overdue_update_rounded = TRUE;
+                       until_minute = 60;
+               } else {
+                       reminders->priv->overdue_update_rounded = FALSE;
+               }
+
+               reminders->priv->overdue_update_id = g_timeout_add_seconds (until_minute,
+                       reminders_widget_overdue_update_cb, reminders);
+
+               return FALSE;
+       } else if (!reminders->priv->overdue_update_rounded) {
+               reminders->priv->overdue_update_rounded = TRUE;
+               reminders->priv->overdue_update_id = g_timeout_add_seconds (60,
+                       reminders_widget_overdue_update_cb, reminders);
+
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static void
+reminders_widget_maybe_schedule_overdue_update (ERemindersWidget *reminders)
+{
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       if (reminders->priv->is_empty || !reminders->priv->is_mapped) {
+               if (reminders->priv->overdue_update_id) {
+                       g_source_remove (reminders->priv->overdue_update_id);
+                       reminders->priv->overdue_update_id = 0;
+               }
+       } else if (!reminders->priv->overdue_update_id) {
+               gint until_minute = 60 - ((g_get_real_time () / G_USEC_PER_SEC) % 60);
+
+               reminders->priv->last_overdue_update = g_get_real_time () / G_USEC_PER_SEC;
+
+               if (until_minute >= 59) {
+                       reminders->priv->overdue_update_rounded = TRUE;
+                       until_minute = 60;
+               } else {
+                       reminders->priv->overdue_update_rounded = FALSE;
+               }
+
+               reminders->priv->overdue_update_id = g_timeout_add_seconds (until_minute,
+                       reminders_widget_overdue_update_cb, reminders);
+       }
+}
+
+static void
+reminders_widget_map (GtkWidget *widget)
+{
+       ERemindersWidget *reminders;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (widget));
+
+       /* Chain up to parent's method. */
+       GTK_WIDGET_CLASS (e_reminders_widget_parent_class)->map (widget);
+
+       reminders = E_REMINDERS_WIDGET (widget);
+       reminders->priv->is_mapped = TRUE;
+
+       reminders_widget_maybe_schedule_overdue_update (reminders);
+}
+
+
+static void
+reminders_widget_unmap (GtkWidget *widget)
+{
+       ERemindersWidget *reminders;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (widget));
+
+       /* Chain up to parent's method. */
+       GTK_WIDGET_CLASS (e_reminders_widget_parent_class)->unmap (widget);
+
+       reminders = E_REMINDERS_WIDGET (widget);
+       reminders->priv->is_mapped = FALSE;
+
+       reminders_widget_maybe_schedule_overdue_update (reminders);
+}
+
+static gint
+reminders_sort_by_occur (gconstpointer ptr1,
+                        gconstpointer ptr2)
+{
+       const EReminderData *rd1 = ptr1, *rd2 = ptr2;
+       gint cmp;
+
+       if (!rd1 || !rd2)
+               return rd1 == rd2 ? 0 : rd1 ? 1 : -1;
+
+       if (rd1->instance.occur_start != rd2->instance.occur_start)
+               return rd1->instance.occur_start < rd2->instance.occur_start ? -1 : 1;
+
+       if (rd1->instance.trigger != rd2->instance.trigger)
+               return rd1->instance.trigger < rd2->instance.trigger ? -1 : 1;
+
+       cmp = g_strcmp0 (rd1->source_uid, rd2->source_uid);
+       if (!cmp)
+               cmp = g_strcmp0 (rd1->instance.auid, rd2->instance.auid);
+
+       return cmp;
+}
+
+static void
+reminders_widget_set_is_empty (ERemindersWidget *reminders,
+                              gboolean is_empty)
+{
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       if (!is_empty == !reminders->priv->is_empty)
+               return;
+
+       reminders->priv->is_empty = is_empty;
+
+       g_object_notify (G_OBJECT (reminders), "empty");
+
+       reminders_widget_maybe_schedule_overdue_update (reminders);
+}
+
+static gint
+reminders_widget_invert_tree_path_compare (gconstpointer ptr1,
+                                          gconstpointer ptr2)
+{
+       return (-1) * gtk_tree_path_compare (ptr1, ptr2);
+}
+
+static void
+reminders_widget_select_one_of (ERemindersWidget *reminders,
+                               GList **inout_previous_paths) /* GtkTreePath * */
+{
+       GList *link;
+       guint len;
+       gint to_select = -1;
+       gint n_rows;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       if (!inout_previous_paths || !*inout_previous_paths)
+               return;
+
+       n_rows = gtk_tree_model_iter_n_children (gtk_tree_view_get_model (reminders->priv->tree_view), NULL);
+       if (n_rows <= 0)
+               return;
+
+       *inout_previous_paths = g_list_sort (*inout_previous_paths, 
reminders_widget_invert_tree_path_compare);
+
+       len = g_list_length (*inout_previous_paths);
+
+       for (link = *inout_previous_paths; link && to_select == -1; link = g_list_next (link), len--) {
+               GtkTreePath *path = link->data;
+               gint *indices, index;
+
+               if (!path || gtk_tree_path_get_depth (path) != 1)
+                       continue;
+
+               indices = gtk_tree_path_get_indices (path);
+               if (!indices)
+                       continue;
+
+               index = indices[0] - len + 1;
+
+               if (index >= n_rows)
+                       to_select = n_rows - 1;
+               else
+                       to_select = index;
+       }
+
+       if (to_select >= 0 && to_select < n_rows) {
+               GtkTreePath *path;
+
+               path = gtk_tree_path_new_from_indices (to_select, -1);
+               if (path) {
+                       gtk_tree_selection_select_path (gtk_tree_view_get_selection 
(reminders->priv->tree_view), path);
+                       gtk_tree_path_free (path);
+               }
+       }
+}
+
+static gboolean
+reminders_widget_refresh_content_cb (gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+       GList *previous_paths;
+       GSList *past;
+       GtkTreeModel *model;
+       GtkTreeSelection *selection;
+       GtkListStore *list_store;
+
+       if (g_source_is_destroyed (g_main_current_source ()))
+               return FALSE;
+
+       g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), FALSE);
+
+       reminders->priv->refresh_idle_id = 0;
+
+       model = gtk_tree_view_get_model (reminders->priv->tree_view);
+       if (!model)
+               return FALSE;
+
+       selection = gtk_tree_view_get_selection (reminders->priv->tree_view);
+       previous_paths = gtk_tree_selection_get_selected_rows (selection, NULL);
+       list_store = GTK_LIST_STORE (model);
+
+       g_object_ref (model);
+       gtk_tree_view_set_model (reminders->priv->tree_view, NULL);
+
+       gtk_list_store_clear (list_store);
+
+       past = e_reminder_watcher_dup_past (reminders->priv->watcher);
+       if (past) {
+               GSList *link;
+               GtkTreeIter iter;
+
+               past = g_slist_sort (past, reminders_sort_by_occur);
+               for (link = past; link; link = g_slist_next (link)) {
+                       const EReminderData *rd = link->data;
+                       gchar *overdue = NULL, *description = NULL;
+
+                       if (!rd || !rd->component)
+                               continue;
+
+                       reminders_get_reminder_markups (reminders, rd, &overdue, &description);
+
+                       gtk_list_store_append (list_store, &iter);
+                       gtk_list_store_set (list_store, &iter,
+                               E_REMINDERS_WIDGET_COLUMN_OVERDUE, overdue,
+                               E_REMINDERS_WIDGET_COLUMN_DESCRIPTION, description,
+                               E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA, rd,
+                               -1);
+
+                       g_free (description);
+                       g_free (overdue);
+               }
+       }
+
+       gtk_tree_view_set_model (reminders->priv->tree_view, model);
+       g_object_unref (model);
+
+       reminders_widget_set_is_empty (reminders, !past);
+
+       if (past) {
+               GtkTreeViewColumn *column;
+
+               column = gtk_tree_view_get_column (reminders->priv->tree_view, 0);
+               if (column)
+                       gtk_tree_view_column_queue_resize (column);
+
+               reminders_widget_select_one_of (reminders, &previous_paths);
+       }
+
+       g_list_free_full (previous_paths, (GDestroyNotify) gtk_tree_path_free);
+       g_slist_free_full (past, e_reminder_data_free);
+
+       g_signal_emit (reminders, signals[CHANGED], 0, NULL);
+
+       return FALSE;
+}
+
+static void
+reminders_widget_schedule_content_refresh (ERemindersWidget *reminders)
+{
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       if (!reminders->priv->refresh_idle_id) {
+               reminders->priv->refresh_idle_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
+                       reminders_widget_refresh_content_cb, reminders, NULL);
+       }
+}
+
+static void
+reminders_widget_watcher_changed_cb (EReminderWatcher *watcher,
+                                    gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       reminders_widget_schedule_content_refresh (reminders);
+}
+
+static void
+reminders_widget_gather_selected_cb (GtkTreeModel *model,
+                                    GtkTreePath *path,
+                                    GtkTreeIter *iter,
+                                    gpointer user_data)
+{
+       GSList **inout_selected = user_data;
+       EReminderData *rd = NULL;
+
+       g_return_if_fail (inout_selected != NULL);
+
+       gtk_tree_model_get (model, iter, E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA, &rd, -1);
+
+       if (rd)
+               *inout_selected = g_slist_prepend (*inout_selected, rd);
+}
+
+static void
+reminders_widget_do_dismiss_cb (ERemindersWidget *reminders,
+                               const EReminderData *rd,
+                               GString *gathered_errors,
+                               GCancellable *cancellable,
+                               gpointer user_data)
+{
+       GError *local_error = NULL;
+
+       if (g_cancellable_is_cancelled (cancellable))
+               return;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+       g_return_if_fail (rd != NULL);
+
+       if (!e_reminder_watcher_dismiss_sync (reminders->priv->watcher, rd, cancellable, &local_error) && 
local_error && gathered_errors &&
+           !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+               if (gathered_errors->len)
+                       g_string_append_c (gathered_errors, '\n');
+               g_string_append (gathered_errors, local_error->message);
+       }
+
+       g_clear_error (&local_error);
+}
+
+typedef void (* ForeachSelectedSyncFunc) (ERemindersWidget *reminders,
+                                         const EReminderData *rd,
+                                         GString *gathered_errors,
+                                         GCancellable *cancellable,
+                                         gpointer user_data);
+
+typedef struct _ForeachSelectedData {
+       GSList *selected; /* EReminderData * */
+       ForeachSelectedSyncFunc sync_func;
+       gpointer user_data;
+       GDestroyNotify user_data_destroy;
+       gchar *error_prefix;
+} ForeachSelectedData;
+
+static void
+foreach_selected_data_free (gpointer ptr)
+{
+       ForeachSelectedData *fsd = ptr;
+
+       if (fsd) {
+               g_slist_free_full (fsd->selected, e_reminder_data_free);
+               if (fsd->user_data_destroy)
+                       fsd->user_data_destroy (fsd->user_data);
+               g_free (fsd->error_prefix);
+               g_free (fsd);
+       }
+}
+
+static void
+reminders_widget_foreach_selected_thread (GTask *task,
+                                         gpointer source_object,
+                                         gpointer task_data,
+                                         GCancellable *cancellable)
+{
+       ForeachSelectedData *fsd = task_data;
+       GString *gathered_errors;
+       GSList *link;
+
+       g_return_if_fail (fsd != NULL);
+       g_return_if_fail (fsd->selected != NULL);
+       g_return_if_fail (fsd->sync_func != NULL);
+
+       if (g_cancellable_is_cancelled (cancellable))
+               return;
+
+       gathered_errors = g_string_new ("");
+
+       for (link = fsd->selected; link && !g_cancellable_is_cancelled (cancellable); link = g_slist_next 
(link)) {
+               const EReminderData *rd = link->data;
+
+               fsd->sync_func (source_object, rd, gathered_errors, cancellable, fsd->user_data);
+       }
+
+       if (gathered_errors->len) {
+               if (fsd->error_prefix) {
+                       g_string_prepend_c (gathered_errors, '\n');
+                       g_string_prepend (gathered_errors, fsd->error_prefix);
+               }
+
+               g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", gathered_errors->str);
+       } else {
+               g_task_return_boolean (task, TRUE);
+       }
+
+       g_string_free (gathered_errors, TRUE);
+}
+
+static void
+reminders_widget_foreach_selected_done_cb (GObject *source_object,
+                                          GAsyncResult *result,
+                                          gpointer user_data)
+{
+       ERemindersWidget *reminders;
+       GError *local_error = NULL;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (source_object));
+
+       reminders = E_REMINDERS_WIDGET (source_object);
+       g_return_if_fail (g_task_is_valid (result, reminders));
+
+       if (!g_task_propagate_boolean (G_TASK (result), &local_error) && local_error) {
+               e_reminders_widget_report_error (reminders, NULL, local_error);
+       }
+
+       g_clear_error (&local_error);
+}
+
+static void
+reminders_widget_foreach_selected (ERemindersWidget *reminders,
+                                  ForeachSelectedSyncFunc sync_func,
+                                  gpointer user_data,
+                                  GDestroyNotify user_data_destroy,
+                                  const gchar *error_prefix)
+{
+       GtkTreeSelection *selection;
+       GSList *selected = NULL; /* EReminderData * */
+       GTask *task;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+       g_return_if_fail (sync_func != NULL);
+
+       selection = gtk_tree_view_get_selection (reminders->priv->tree_view);
+       gtk_tree_selection_selected_foreach (selection, reminders_widget_gather_selected_cb, &selected);
+
+       if (selected) {
+               ForeachSelectedData *fsd;
+
+               fsd = g_new0 (ForeachSelectedData, 1);
+               fsd->selected = selected; /* Takes ownership */
+               fsd->sync_func = sync_func;
+               fsd->user_data = user_data;
+               fsd->user_data_destroy = user_data_destroy;
+               fsd->error_prefix = g_strdup (error_prefix);
+
+               task = g_task_new (reminders, reminders->priv->cancellable, 
reminders_widget_foreach_selected_done_cb, NULL);
+               g_task_set_task_data (task, fsd, foreach_selected_data_free);
+               g_task_set_check_cancellable (task, FALSE);
+               g_task_run_in_thread (task, reminders_widget_foreach_selected_thread);
+               g_object_unref (task);
+       }
+}
+
+static void
+reminders_widget_row_activated_cb (GtkTreeView *tree_view,
+                                  GtkTreePath *path,
+                                  GtkTreeViewColumn *column,
+                                  gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+       GtkTreeModel *model;
+       GtkTreeIter iter;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       if (!path)
+               return;
+
+       model = gtk_tree_view_get_model (reminders->priv->tree_view);
+       if (gtk_tree_model_get_iter (model, &iter, path)) {
+               EReminderData *rd = NULL;
+
+               gtk_tree_model_get (model, &iter,
+                       E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA, &rd,
+                       -1);
+
+               if (rd) {
+                       gboolean result = FALSE;
+
+                       g_signal_emit (reminders, signals[ACTIVATED], 0, rd, &result);
+
+                       if (!result) {
+                               const gchar *scheme = NULL;
+                               const gchar *comp_uid = NULL;
+
+                               e_cal_component_get_uid (rd->component, &comp_uid);
+
+                               switch (e_cal_component_get_vtype (rd->component)) {
+                                       case E_CAL_COMPONENT_EVENT:
+                                               scheme = "calendar:";
+                                               break;
+                                       case E_CAL_COMPONENT_TODO:
+                                               scheme = "task:";
+                                               break;
+                                       case E_CAL_COMPONENT_JOURNAL:
+                                               scheme = "memo:";
+                                               break;
+                                       default:
+                                               break;
+                               }
+
+                               if (scheme && comp_uid && rd->source_uid) {
+                                       GString *uri;
+                                       gchar *tmp;
+                                       GError *error = NULL;
+
+                                       uri = g_string_sized_new (128);
+                                       g_string_append (uri, scheme);
+                                       g_string_append (uri, "///?");
+
+                                       tmp = g_uri_escape_string (rd->source_uid, NULL, TRUE);
+                                       g_string_append (uri, "source-uid=");
+                                       g_string_append (uri, tmp);
+                                       g_free (tmp);
+
+                                       g_string_append (uri, "&");
+
+                                       tmp = g_uri_escape_string (comp_uid, NULL, TRUE);
+                                       g_string_append (uri, "comp-uid=");
+                                       g_string_append (uri, tmp);
+                                       g_free (tmp);
+
+                                       if (!g_app_info_launch_default_for_uri (uri->str, NULL, &error) &&
+                                           !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {
+                                               gchar *prefix = g_strdup_printf (_("Failed to launch URI 
“%s”:"), uri->str);
+                                               e_reminders_widget_report_error (reminders, prefix, error);
+                                               g_free (prefix);
+                                       }
+
+                                       g_string_free (uri, TRUE);
+                                       g_clear_error (&error);
+                               }
+                       }
+
+                       e_reminder_data_free (rd);
+               }
+       }
+}
+
+static void
+reminders_widget_selection_changed_cb (GtkTreeSelection *selection,
+                                      gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+       gint nselected;
+
+       g_return_if_fail (GTK_IS_TREE_SELECTION (selection));
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       nselected = gtk_tree_selection_count_selected_rows (selection);
+       gtk_widget_set_sensitive (reminders->priv->snooze_combo, nselected > 0);
+       gtk_widget_set_sensitive (reminders->priv->snooze_button, nselected > 0);
+       gtk_widget_set_sensitive (reminders->priv->dismiss_button, nselected > 0);
+}
+
+static void
+reminders_widget_dismiss_button_clicked_cb (GtkButton *button,
+                                           gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       g_signal_handlers_block_by_func (reminders->priv->watcher, reminders_widget_watcher_changed_cb, 
reminders);
+
+       reminders_widget_foreach_selected (reminders, reminders_widget_do_dismiss_cb, NULL, NULL, _("Failed 
to dismiss reminder:"));
+
+       g_signal_handlers_unblock_by_func (reminders->priv->watcher, reminders_widget_watcher_changed_cb, 
reminders);
+
+       reminders_widget_watcher_changed_cb (reminders->priv->watcher, reminders);
+}
+
+static void
+reminders_widget_dismiss_all_done_cb (GObject *source_object,
+                                     GAsyncResult *result,
+                                     gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+       GError *local_error = NULL;
+
+       g_return_if_fail (E_IS_REMINDER_WATCHER (source_object));
+
+       if (!e_reminder_watcher_dismiss_all_finish (reminders->priv->watcher, result, &local_error) &&
+           !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+               g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+               e_reminders_widget_report_error (reminders, _("Failed to dismiss all:"), local_error);
+       }
+
+       g_clear_error (&local_error);
+}
+
+static void
+reminders_widget_dismiss_all_button_clicked_cb (GtkButton *button,
+                                               gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       e_reminder_watcher_dismiss_all (reminders->priv->watcher, reminders->priv->cancellable,
+               reminders_widget_dismiss_all_done_cb, reminders);
+}
+
+static void
+reminders_widget_add_snooze_add_button_clicked_cb (GtkButton *button,
+                                                  gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+       GtkTreeModel *model;
+       GtkTreeIter iter;
+       gboolean found = FALSE;
+       gint new_minutes;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       new_minutes =
+               gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_minutes_spin)) 
+
+               (60 * gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON 
(reminders->priv->add_snooze_hours_spin))) +
+               (24 * 60 * gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON 
(reminders->priv->add_snooze_days_spin)));
+       g_return_if_fail (new_minutes > 0);
+
+       gtk_widget_hide (reminders->priv->add_snooze_popover);
+
+       model = gtk_combo_box_get_model (GTK_COMBO_BOX (reminders->priv->snooze_combo));
+       g_return_if_fail (model != NULL);
+
+       if (gtk_tree_model_get_iter_first (model, &iter)) {
+               do {
+                       gint minutes = 0;
+
+                       gtk_tree_model_get (model, &iter, 1, &minutes, -1);
+
+                       if (minutes == new_minutes) {
+                               found = TRUE;
+                               gtk_combo_box_set_active_iter (GTK_COMBO_BOX (reminders->priv->snooze_combo), 
&iter);
+                               break;
+                       }
+               } while (gtk_tree_model_iter_next (model, &iter));
+       }
+
+       if (!found) {
+               GVariant *variant;
+               gint32 array[MAX_CUSTOM_SNOOZE_VALUES] = { 0 }, narray = 0, ii;
+
+               variant = g_settings_get_value (reminders->priv->settings, "notify-custom-snooze-minutes");
+               if (variant) {
+                       const gint32 *stored;
+                       gsize nstored = 0;
+
+                       stored = g_variant_get_fixed_array (variant, &nstored, sizeof (gint32));
+                       if (stored && nstored > 0) {
+                               /* Skip the oldest, when too many stored */
+                               for (ii = nstored >= MAX_CUSTOM_SNOOZE_VALUES ? 1 : 0; ii < 
MAX_CUSTOM_SNOOZE_VALUES && ii < nstored; ii++) {
+                                       array[narray] = stored[ii];
+                                       narray++;
+                               }
+                       }
+
+                       g_variant_unref (variant);
+               }
+
+               /* Add the new at the end of the array */
+               array[narray] = new_minutes;
+               narray++;
+
+               variant = g_variant_new_fixed_array (G_VARIANT_TYPE_INT32, array, narray, sizeof (gint32));
+               g_settings_set_value (reminders->priv->settings, "notify-custom-snooze-minutes", variant);
+
+               reminders_widget_fill_snooze_combo (reminders, new_minutes);
+       }
+}
+
+static void
+reminders_widget_add_snooze_update_sensitize_cb (GtkSpinButton *spin,
+                                                gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       gtk_widget_set_sensitive (reminders->priv->add_snooze_add_button,
+               gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_minutes_spin)) 
+
+               gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_hours_spin)) +
+               gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_days_spin)) > 
0);
+}
+
+static void
+reminders_widget_snooze_add_custom (ERemindersWidget *reminders)
+{
+       GtkTreeIter iter;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       if (!reminders->priv->add_snooze_popover) {
+               GtkWidget *widget;
+               GtkBox *vbox, *box;
+
+               reminders->priv->add_snooze_days_spin = gtk_spin_button_new_with_range (0.0, 366.0, 1.0);
+               reminders->priv->add_snooze_hours_spin = gtk_spin_button_new_with_range (0.0, 23.0, 1.0);
+               reminders->priv->add_snooze_minutes_spin = gtk_spin_button_new_with_range (0.0, 59.0, 1.0);
+
+               g_object_set (G_OBJECT (reminders->priv->add_snooze_days_spin),
+                       "digits", 0,
+                       "numeric", TRUE,
+                       "snap-to-ticks", TRUE,
+                       NULL);
+
+               g_object_set (G_OBJECT (reminders->priv->add_snooze_hours_spin),
+                       "digits", 0,
+                       "numeric", TRUE,
+                       "snap-to-ticks", TRUE,
+                       NULL);
+
+               g_object_set (G_OBJECT (reminders->priv->add_snooze_minutes_spin),
+                       "digits", 0,
+                       "numeric", TRUE,
+                       "snap-to-ticks", TRUE,
+                       NULL);
+
+               vbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 2));
+
+               widget = gtk_label_new (_("Set a custom snooze time for"));
+               gtk_box_pack_start (vbox, widget, FALSE, FALSE, 0);
+
+               box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+               g_object_set (G_OBJECT (box),
+                       "halign", GTK_ALIGN_START,
+                       "hexpand", FALSE,
+                       "valign", GTK_ALIGN_CENTER,
+                       "vexpand", FALSE,
+                       NULL);
+
+               gtk_box_pack_start (box, reminders->priv->add_snooze_days_spin, FALSE, FALSE, 4);
+               /* Translators: this is part of: "Set a custom snooze time for [nnn] days [nnn] hours [nnn] 
minutes", where the text in "[]" means a separate widget */
+               widget = gtk_label_new_with_mnemonic (C_("reminders-snooze", "da_ys"));
+               gtk_label_set_mnemonic_widget (GTK_LABEL (widget), reminders->priv->add_snooze_days_spin);
+               gtk_box_pack_start (box, widget, FALSE, FALSE, 4);
+
+               gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+               box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+               g_object_set (G_OBJECT (box),
+                       "halign", GTK_ALIGN_START,
+                       "hexpand", FALSE,
+                       "valign", GTK_ALIGN_CENTER,
+                       "vexpand", FALSE,
+                       NULL);
+
+               gtk_box_pack_start (box, reminders->priv->add_snooze_hours_spin, FALSE, FALSE, 4);
+               /* Translators: this is part of: "Set a custom snooze time for [nnn] days [nnn] hours [nnn] 
minutes", where the text in "[]" means a separate widget */
+               widget = gtk_label_new_with_mnemonic (C_("reminders-snooze", "_hours"));
+               gtk_label_set_mnemonic_widget (GTK_LABEL (widget), reminders->priv->add_snooze_hours_spin);
+               gtk_box_pack_start (box, widget, FALSE, FALSE, 4);
+
+               gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+               box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+               g_object_set (G_OBJECT (box),
+                       "halign", GTK_ALIGN_START,
+                       "hexpand", FALSE,
+                       "valign", GTK_ALIGN_CENTER,
+                       "vexpand", FALSE,
+                       NULL);
+
+               gtk_box_pack_start (box, reminders->priv->add_snooze_minutes_spin, FALSE, FALSE, 4);
+               /* Translators: this is part of: "Set a custom snooze time for [nnn] days [nnn] hours [nnn] 
minutes", where the text in "[]" means a separate widget */
+               widget = gtk_label_new_with_mnemonic (C_("reminders-snooze", "_minutes"));
+               gtk_label_set_mnemonic_widget (GTK_LABEL (widget), reminders->priv->add_snooze_minutes_spin);
+               gtk_box_pack_start (box, widget, FALSE, FALSE, 4);
+
+               gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+               reminders->priv->add_snooze_add_button = gtk_button_new_with_mnemonic (_("_Add Snooze time"));
+               g_object_set (G_OBJECT (reminders->priv->add_snooze_add_button),
+                       "halign", GTK_ALIGN_CENTER,
+                       NULL);
+
+               gtk_box_pack_start (vbox, reminders->priv->add_snooze_add_button, FALSE, FALSE, 0);
+
+               gtk_widget_show_all (GTK_WIDGET (vbox));
+
+               reminders->priv->add_snooze_popover = gtk_popover_new (GTK_WIDGET (reminders));
+               gtk_popover_set_position (GTK_POPOVER (reminders->priv->add_snooze_popover), GTK_POS_BOTTOM);
+               gtk_container_add (GTK_CONTAINER (reminders->priv->add_snooze_popover), GTK_WIDGET (vbox));
+               gtk_container_set_border_width (GTK_CONTAINER (reminders->priv->add_snooze_popover), 6);
+
+               g_signal_connect (reminders->priv->add_snooze_add_button, "clicked",
+                       G_CALLBACK (reminders_widget_add_snooze_add_button_clicked_cb), reminders);
+
+               g_signal_connect (reminders->priv->add_snooze_days_spin, "value-changed",
+                       G_CALLBACK (reminders_widget_add_snooze_update_sensitize_cb), reminders);
+
+               g_signal_connect (reminders->priv->add_snooze_hours_spin, "value-changed",
+                       G_CALLBACK (reminders_widget_add_snooze_update_sensitize_cb), reminders);
+
+               g_signal_connect (reminders->priv->add_snooze_minutes_spin, "value-changed",
+                       G_CALLBACK (reminders_widget_add_snooze_update_sensitize_cb), reminders);
+
+               reminders_widget_add_snooze_update_sensitize_cb (NULL, reminders);
+       }
+
+       if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (reminders->priv->snooze_combo), &iter)) {
+               gint minutes = -1;
+
+               gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (reminders->priv->snooze_combo)), 
&iter, 1, &minutes, -1);
+
+               if (minutes > 0) {
+                       gtk_spin_button_set_value (GTK_SPIN_BUTTON 
(reminders->priv->add_snooze_minutes_spin), minutes % 60);
+
+                       minutes = minutes / 60;
+                       gtk_spin_button_set_value (GTK_SPIN_BUTTON (reminders->priv->add_snooze_hours_spin), 
minutes % 24);
+
+                       minutes = minutes / 24;
+                       gtk_spin_button_set_value (GTK_SPIN_BUTTON (reminders->priv->add_snooze_days_spin), 
minutes);
+               }
+       }
+
+       gtk_widget_hide (reminders->priv->add_snooze_popover);
+       gtk_popover_set_relative_to (GTK_POPOVER (reminders->priv->add_snooze_popover), 
reminders->priv->snooze_combo);
+       gtk_widget_show (reminders->priv->add_snooze_popover);
+
+       gtk_widget_grab_focus (reminders->priv->add_snooze_days_spin);
+}
+
+static void
+reminders_widget_snooze_combo_changed_cb (GtkComboBox *combo,
+                                         gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+       GtkTreeIter iter;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       if (reminders->priv->updating_snooze_combo)
+               return;
+
+       if (gtk_combo_box_get_active_iter (combo, &iter)) {
+               GtkTreeModel *model;
+               gint minutes = -3;
+
+               model = gtk_combo_box_get_model (combo);
+
+               gtk_tree_model_get (model, &iter, 1, &minutes, -1);
+
+               if (minutes > 0) {
+                       reminders->priv->last_selected_snooze_minutes = minutes;
+               } else if (minutes == -1 || minutes == -2) {
+                       if (reminders->priv->last_selected_snooze_minutes) {
+                               reminders->priv->updating_snooze_combo = TRUE;
+
+                               if (gtk_tree_model_get_iter_first (model, &iter)) {
+                                       do {
+                                               gint stored = -1;
+
+                                               gtk_tree_model_get (model, &iter, 1, &stored, -1);
+                                               if (stored == reminders->priv->last_selected_snooze_minutes) {
+                                                       gtk_combo_box_set_active_iter (combo, &iter);
+                                                       break;
+                                               }
+                                       } while (gtk_tree_model_iter_next (model, &iter));
+                               }
+
+                               reminders->priv->updating_snooze_combo = FALSE;
+                       }
+
+                       /* The "Add custom" item was selected */
+                       if (minutes == -1) {
+                               reminders_widget_snooze_add_custom (reminders);
+                       /* The "Clear custom times" item was selected */
+                       } else if (minutes == -2) {
+                               g_settings_reset (reminders->priv->settings, "notify-custom-snooze-minutes");
+                       }
+               }
+       }
+}
+
+static void
+reminders_widget_snooze_button_clicked_cb (GtkButton *button,
+                                          gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+       GtkTreeSelection *selection;
+       GSList *selected = NULL, *link;
+       GtkTreeIter iter;
+       gint minutes = 0;
+       gint64 until;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+       g_return_if_fail (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (reminders->priv->snooze_combo), 
&iter));
+
+       gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (reminders->priv->snooze_combo)), &iter, 
1, &minutes, -1);
+
+       g_return_if_fail (minutes > 0);
+
+       until = (g_get_real_time () / G_USEC_PER_SEC) + (minutes * 60);
+
+       g_settings_set_int (reminders->priv->settings, "notify-last-snooze-minutes", minutes);
+
+       selection = gtk_tree_view_get_selection (reminders->priv->tree_view);
+       gtk_tree_selection_selected_foreach (selection, reminders_widget_gather_selected_cb, &selected);
+
+       g_signal_handlers_block_by_func (reminders->priv->watcher, reminders_widget_watcher_changed_cb, 
reminders);
+
+       for (link = selected; link; link = g_slist_next (link)) {
+               const EReminderData *rd = link->data;
+
+               e_reminder_watcher_snooze (reminders->priv->watcher, rd, until);
+       }
+
+       g_slist_free_full (selected, e_reminder_data_free);
+
+       g_signal_handlers_unblock_by_func (reminders->priv->watcher, reminders_widget_watcher_changed_cb, 
reminders);
+
+       if (selected)
+               reminders_widget_watcher_changed_cb (reminders->priv->watcher, reminders);
+}
+
+static void
+reminders_widget_set_watcher (ERemindersWidget *reminders,
+                             EReminderWatcher *watcher)
+{
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+       g_return_if_fail (E_IS_REMINDER_WATCHER (watcher));
+       g_return_if_fail (reminders->priv->watcher == NULL);
+
+       reminders->priv->watcher = g_object_ref (watcher);
+}
+
+static void
+reminders_widget_set_property (GObject *object,
+                              guint property_id,
+                              const GValue *value,
+                              GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_WATCHER:
+                       reminders_widget_set_watcher (
+                               E_REMINDERS_WIDGET (object),
+                               g_value_get_object (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+reminders_widget_get_property (GObject *object,
+                              guint property_id,
+                              GValue *value,
+                              GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_WATCHER:
+                       g_value_set_object (
+                               value, e_reminders_widget_get_watcher (
+                               E_REMINDERS_WIDGET (object)));
+                       return;
+
+               case PROP_EMPTY:
+                       g_value_set_boolean (
+                               value, e_reminders_widget_is_empty (
+                               E_REMINDERS_WIDGET (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+reminders_widget_constructed (GObject *object)
+{
+       ERemindersWidget *reminders = E_REMINDERS_WIDGET (object);
+       GtkWidget *scrolled_window;
+       GtkListStore *list_store;
+       GtkTreeSelection *selection;
+       GtkTreeViewColumn *column;
+       GtkCellRenderer *renderer;
+       GtkWidget *widget;
+       GtkBox *box;
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_reminders_widget_parent_class)->constructed (object);
+
+       scrolled_window = gtk_scrolled_window_new (NULL, NULL);
+       g_object_set (G_OBJECT (scrolled_window),
+               "halign", GTK_ALIGN_FILL,
+               "hexpand", TRUE,
+               "valign", GTK_ALIGN_FILL,
+               "vexpand", TRUE,
+               "hscrollbar-policy", GTK_POLICY_NEVER,
+               "vscrollbar-policy", GTK_POLICY_AUTOMATIC,
+               "shadow-type", GTK_SHADOW_IN,
+               NULL);
+
+       gtk_grid_attach (GTK_GRID (reminders), scrolled_window, 0, 0, 1, 1);
+
+       list_store = gtk_list_store_new (E_REMINDERS_WIDGET_N_COLUMNS,
+               G_TYPE_STRING, /* E_REMINDERS_WIDGET_COLUMN_OVERDUE */
+               G_TYPE_STRING, /* E_REMINDERS_WIDGET_COLUMN_DESCRIPTION */
+               E_TYPE_REMINDER_DATA); /* E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA */
+
+       reminders->priv->tree_view = GTK_TREE_VIEW (gtk_tree_view_new_with_model (GTK_TREE_MODEL 
(list_store)));
+
+       g_object_unref (list_store);
+
+       g_object_set (G_OBJECT (reminders->priv->tree_view),
+               "halign", GTK_ALIGN_FILL,
+               "hexpand", TRUE,
+               "valign", GTK_ALIGN_FILL,
+               "vexpand", TRUE,
+               "activate-on-single-click", FALSE,
+               "enable-search", FALSE,
+               "fixed-height-mode", TRUE,
+               "headers-visible", FALSE,
+               "hover-selection", FALSE,
+               NULL);
+
+       gtk_container_add (GTK_CONTAINER (scrolled_window), GTK_WIDGET (reminders->priv->tree_view));
+
+       gtk_tree_view_set_tooltip_column (reminders->priv->tree_view, E_REMINDERS_WIDGET_COLUMN_DESCRIPTION);
+
+       /* Headers not visible, thus column's caption is not localized */
+       gtk_tree_view_insert_column_with_attributes (reminders->priv->tree_view, -1, "Overdue",
+               gtk_cell_renderer_text_new (), "markup", E_REMINDERS_WIDGET_COLUMN_OVERDUE, NULL);
+
+       renderer = gtk_cell_renderer_text_new ();
+       g_object_set (G_OBJECT (renderer),
+               "ellipsize", PANGO_ELLIPSIZE_END,
+               NULL);
+
+       gtk_tree_view_insert_column_with_attributes (reminders->priv->tree_view, -1, "Description",
+               renderer, "markup", E_REMINDERS_WIDGET_COLUMN_DESCRIPTION, NULL);
+
+       column = gtk_tree_view_get_column (reminders->priv->tree_view, 0);
+       gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
+
+       column = gtk_tree_view_get_column (reminders->priv->tree_view, 1);
+       gtk_tree_view_column_set_expand (column, TRUE);
+
+       reminders->priv->dismiss_button = gtk_button_new_with_mnemonic (_("_Dismiss"));
+       reminders->priv->dismiss_all_button = gtk_button_new_with_mnemonic (_("Dismiss _All"));
+       reminders->priv->snooze_combo = reminders_widget_new_snooze_combo ();
+       reminders->priv->snooze_button = gtk_button_new_with_mnemonic (_("_Snooze"));
+
+       reminders_widget_fill_snooze_combo (reminders,
+               g_settings_get_int (reminders->priv->settings, "notify-last-snooze-minutes"));
+
+       box = GTK_BOX (gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL));
+       g_object_set (G_OBJECT (box),
+               "halign", GTK_ALIGN_END,
+               "hexpand", TRUE,
+               "valign", GTK_ALIGN_CENTER,
+               "vexpand", FALSE,
+               "margin-top", 4,
+               NULL);
+
+       widget = gtk_label_new ("");
+
+       gtk_box_pack_start (box, reminders->priv->snooze_combo, FALSE, FALSE, 0);
+       gtk_box_pack_start (box, reminders->priv->snooze_button, FALSE, FALSE, 0);
+       gtk_box_pack_start (box, widget, FALSE, FALSE, 0);
+       gtk_box_pack_start (box, reminders->priv->dismiss_button, FALSE, FALSE, 0);
+       gtk_box_pack_start (box, reminders->priv->dismiss_all_button, FALSE, FALSE, 0);
+
+       gtk_button_box_set_child_non_homogeneous (GTK_BUTTON_BOX (box), reminders->priv->snooze_combo, TRUE);
+       gtk_button_box_set_child_non_homogeneous (GTK_BUTTON_BOX (box), widget, TRUE);
+
+       gtk_grid_attach (GTK_GRID (reminders), GTK_WIDGET (box), 0, 1, 1, 1);
+
+       gtk_widget_show_all (GTK_WIDGET (reminders));
+
+       selection = gtk_tree_view_get_selection (reminders->priv->tree_view);
+       gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
+
+       g_signal_connect (reminders->priv->tree_view, "row-activated",
+               G_CALLBACK (reminders_widget_row_activated_cb), reminders);
+
+       g_signal_connect (selection, "changed",
+               G_CALLBACK (reminders_widget_selection_changed_cb), reminders);
+
+       g_signal_connect (reminders->priv->snooze_button, "clicked",
+               G_CALLBACK (reminders_widget_snooze_button_clicked_cb), reminders);
+
+       g_signal_connect (reminders->priv->dismiss_button, "clicked",
+               G_CALLBACK (reminders_widget_dismiss_button_clicked_cb), reminders);
+
+       g_signal_connect (reminders->priv->dismiss_all_button, "clicked",
+               G_CALLBACK (reminders_widget_dismiss_all_button_clicked_cb), reminders);
+
+       g_signal_connect (reminders->priv->watcher, "changed",
+               G_CALLBACK (reminders_widget_watcher_changed_cb), reminders);
+
+       g_signal_connect (reminders->priv->snooze_combo, "changed",
+               G_CALLBACK (reminders_widget_snooze_combo_changed_cb), reminders);
+
+       g_signal_connect (reminders->priv->settings, "changed::notify-custom-snooze-minutes",
+               G_CALLBACK (reminders_widget_custom_snooze_minutes_changed_cb), reminders);
+
+       e_binding_bind_property (reminders, "empty",
+               reminders->priv->dismiss_all_button, "sensitive",
+               G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
+
+       e_binding_bind_property (reminders, "empty",
+               scrolled_window, "sensitive",
+               G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
+
+       _libedataserverui_load_modules ();
+
+       e_extensible_load_extensions (E_EXTENSIBLE (object));
+
+       reminders_widget_schedule_content_refresh (reminders);
+}
+
+static void
+reminders_widget_dispose (GObject *object)
+{
+       ERemindersWidget *reminders = E_REMINDERS_WIDGET (object);
+
+       g_cancellable_cancel (reminders->priv->cancellable);
+
+       if (reminders->priv->refresh_idle_id) {
+               g_source_remove (reminders->priv->refresh_idle_id);
+               reminders->priv->refresh_idle_id = 0;
+       }
+
+       if (reminders->priv->overdue_update_id) {
+               g_source_remove (reminders->priv->overdue_update_id);
+               reminders->priv->overdue_update_id = 0;
+       }
+
+       if (reminders->priv->watcher)
+               g_signal_handlers_disconnect_by_data (reminders->priv->watcher, reminders);
+
+       if (reminders->priv->settings)
+               g_signal_handlers_disconnect_by_data (reminders->priv->settings, reminders);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_reminders_widget_parent_class)->dispose (object);
+}
+
+static void
+reminders_widget_finalize (GObject *object)
+{
+       ERemindersWidget *reminders = E_REMINDERS_WIDGET (object);
+
+       g_clear_object (&reminders->priv->watcher);
+       g_clear_object (&reminders->priv->settings);
+       g_clear_object (&reminders->priv->cancellable);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_reminders_widget_parent_class)->finalize (object);
+}
+
+static void
+e_reminders_widget_class_init (ERemindersWidgetClass *klass)
+{
+       GObjectClass *object_class;
+       GtkWidgetClass *widget_class;
+
+       g_type_class_add_private (klass, sizeof (ERemindersWidgetPrivate));
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->set_property = reminders_widget_set_property;
+       object_class->get_property = reminders_widget_get_property;
+       object_class->constructed = reminders_widget_constructed;
+       object_class->dispose = reminders_widget_dispose;
+       object_class->finalize = reminders_widget_finalize;
+
+       widget_class = GTK_WIDGET_CLASS (klass);
+       widget_class->map = reminders_widget_map;
+       widget_class->unmap = reminders_widget_unmap;
+
+       /**
+        * ERemindersWidget::watcher:
+        *
+        * An #EReminderWatcher used to work with reminders.
+        *
+        * Since: 3.30
+        **/
+       g_object_class_install_property (
+               object_class,
+               PROP_WATCHER,
+               g_param_spec_object (
+                       "watcher",
+                       "Reminder Watcher",
+                       "The reminder watcher used to work with reminders",
+                       E_TYPE_REMINDER_WATCHER,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT_ONLY |
+                       G_PARAM_STATIC_STRINGS));
+
+       /**
+        * ERemindersWidget::empty:
+        *
+        * Set to %TRUE when there's no past reminder in the widget.
+        *
+        * Since: 3.30
+        **/
+       g_object_class_install_property (
+               object_class,
+               PROP_EMPTY,
+               g_param_spec_boolean (
+                       "empty",
+                       "Empty",
+                       "Whether there are no past reminders",
+                       TRUE,
+                       G_PARAM_READABLE |
+                       G_PARAM_STATIC_STRINGS));
+
+       /**
+        * ERemindersWidget:changed:
+        * @reminders: an #ERemindersWidget
+        *
+        * A signal being called to notify about changes in the past reminders list.
+        *
+        * Since: 3.30
+        **/
+       signals[CHANGED] = g_signal_new (
+               "changed",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+               G_STRUCT_OFFSET (ERemindersWidgetClass, changed),
+               NULL,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_NONE, 0,
+               G_TYPE_NONE);
+
+       /**
+        * ERemindersWidget:activated:
+        * @reminders: an #ERemindersWidget
+        * @rd: an #EReminderData
+        *
+        * A signal being called when the user activates one of the past reminders in the tree view.
+        * The @rd corresponds to the activated reminder.
+        *
+        * Returns: %TRUE, when the further processing of this signal should be stopped, %FALSE otherwise.
+        *
+        * Since: 3.30
+        **/
+       signals[ACTIVATED] = g_signal_new (
+               "activated",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+               G_STRUCT_OFFSET (ERemindersWidgetClass, activated),
+               g_signal_accumulator_first_wins,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_BOOLEAN, 1,
+               E_TYPE_REMINDER_DATA);
+}
+
+static void
+e_reminders_widget_init (ERemindersWidget *reminders)
+{
+       reminders->priv = G_TYPE_INSTANCE_GET_PRIVATE (reminders, E_TYPE_REMINDERS_WIDGET, 
ERemindersWidgetPrivate);
+       reminders->priv->settings = g_settings_new ("org.gnome.evolution-data-server.calendar");
+       reminders->priv->cancellable = g_cancellable_new ();
+       reminders->priv->is_empty = TRUE;
+       reminders->priv->is_mapped = FALSE;
+}
+
+/**
+ * e_reminders_widget_new:
+ * @watcher: an #EReminderWatcher
+ *
+ * Creates a new instance of #ERemindersWidget. It adds its own reference
+ * on the @watcher.
+ *
+ * Returns: (transfer full): a new instance of #ERemindersWidget.
+ *
+ * Since: 3.30
+ **/
+ERemindersWidget *
+e_reminders_widget_new (EReminderWatcher *watcher)
+{
+       g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), NULL);
+
+       return g_object_new (E_TYPE_REMINDERS_WIDGET,
+               "watcher", watcher,
+               NULL);
+}
+
+/**
+ * e_reminders_widget_get_watcher:
+ * @reminders: an #ERemindersWidget
+ *
+ * Returns: (transfer none): an #EReminderWatcher with which the @reminders had
+ *    been created. Do on unref it, it's owned by the @reminders.
+ *
+ * Since: 3.30
+ **/
+EReminderWatcher *
+e_reminders_widget_get_watcher (ERemindersWidget *reminders)
+{
+       g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), NULL);
+
+       return reminders->priv->watcher;
+}
+
+/**
+ * e_reminders_widget_get_settings:
+ * @reminders: an #ERemindersWidget
+ *
+ * Returns: (transfer none): a #GSettings pointing to org.gnome.evolution-data-server.calendar
+ *    used by the @reminders widget.
+ *
+ * Since: 3.30
+ **/
+GSettings *
+e_reminders_widget_get_settings (ERemindersWidget *reminders)
+{
+       g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), NULL);
+
+       return reminders->priv->settings;
+}
+
+/**
+ * e_reminders_widget_is_empty:
+ * @reminders: an #ERemindersWidget
+ *
+ * Returns: %TRUE, when there is no past reminder left, %FALSE otherwise.
+ *
+ * Since: 3.30
+ **/
+gboolean
+e_reminders_widget_is_empty (ERemindersWidget *reminders)
+{
+       g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), FALSE);
+
+       return reminders->priv->is_empty;
+}
+
+/**
+ * e_reminders_widget_get_tree_view:
+ * @reminders: an #ERemindersWidget
+ *
+ * Returns: (transfer none): a #GtkTreeView with past reminders. It's owned
+ *    by the @reminders widget.
+ *
+ * Since: 3.30
+ **/
+GtkTreeView *
+e_reminders_widget_get_tree_view (ERemindersWidget *reminders)
+{
+       g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), NULL);
+
+       return reminders->priv->tree_view;
+}
+
+static void
+reminders_widget_error_response_cb (GtkInfoBar *info_bar,
+                                   gint response_id,
+                                   gpointer user_data)
+{
+       ERemindersWidget *reminders = user_data;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       if (reminders->priv->info_bar == info_bar) {
+               gtk_widget_destroy (GTK_WIDGET (reminders->priv->info_bar));
+               reminders->priv->info_bar = NULL;
+       }
+}
+
+/**
+ * e_reminders_widget_report_error:
+ * @reminders: an #ERemindersWidget
+ * @prefix: (nullable): an optional prefix to show before the error message, or %NULL for none
+ * @error: (nullable): a #GError to show the message from in the UI, or %NULL for unknown error
+ *
+ * Shows a warning in the GUI with the @error message, optionally prefixed
+ * with @prefix. When @error is %NULL, an "Unknown error" message is shown
+ * instead.
+ *
+ * Since: 3.30
+ **/
+void
+e_reminders_widget_report_error (ERemindersWidget *reminders,
+                                const gchar *prefix,
+                                const GError *error)
+{
+       GtkLabel *label;
+       const gchar *message;
+       gchar *tmp = NULL;
+
+       g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+       if (error)
+               message = error->message;
+       else
+               message = _("Unknown error");
+
+       if (prefix && *prefix) {
+               if (gtk_widget_get_direction (GTK_WIDGET (reminders)) == GTK_TEXT_DIR_RTL)
+                       tmp = g_strconcat (message, " ", prefix, NULL);
+               else
+                       tmp = g_strconcat (prefix, " ", message, NULL);
+
+               message = tmp;
+       }
+
+       if (reminders->priv->info_bar) {
+               gtk_widget_destroy (GTK_WIDGET (reminders->priv->info_bar));
+               reminders->priv->info_bar = NULL;
+       }
+
+       reminders->priv->info_bar = GTK_INFO_BAR (gtk_info_bar_new ());
+       gtk_info_bar_set_message_type (reminders->priv->info_bar, GTK_MESSAGE_ERROR);
+       gtk_info_bar_set_show_close_button (reminders->priv->info_bar, TRUE);
+
+       label = GTK_LABEL (gtk_label_new (message));
+       gtk_label_set_max_width_chars (label, 120);
+       gtk_label_set_line_wrap (label, TRUE);
+       gtk_label_set_selectable (label, TRUE);
+       gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (reminders->priv->info_bar)), 
GTK_WIDGET (label));
+       gtk_widget_show (GTK_WIDGET (label));
+       gtk_widget_show (GTK_WIDGET (reminders->priv->info_bar));
+
+       g_signal_connect (reminders->priv->info_bar, "response", G_CALLBACK 
(reminders_widget_error_response_cb), reminders);
+
+       gtk_grid_attach (GTK_GRID (reminders), GTK_WIDGET (reminders->priv->info_bar), 0, 2, 1, 1);
+
+       g_free (tmp);
+}
diff --git a/src/libedataserverui/e-reminders-widget.h b/src/libedataserverui/e-reminders-widget.h
new file mode 100644
index 0000000..ea20c13
--- /dev/null
+++ b/src/libedataserverui/e-reminders-widget.h
@@ -0,0 +1,109 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATASERVERUI_H_INSIDE__) && !defined (LIBEDATASERVERUI_COMPILATION)
+#error "Only <libedataserverui/libedataserverui.h> should be included directly."
+#endif
+
+#ifndef E_REMINDERS_WIDGET_H
+#define E_REMINDERS_WIDGET_H
+
+#include <gtk/gtk.h>
+#include <libecal/libecal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_REMINDERS_WIDGET \
+       (e_reminders_widget_get_type ())
+#define E_REMINDERS_WIDGET(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_REMINDERS_WIDGET, ERemindersWidget))
+#define E_REMINDERS_WIDGET_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_REMINDERS_WIDGET, ERemindersWidgetClass))
+#define E_IS_REMINDERS_WIDGET(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_REMINDERS_WIDGET))
+#define E_IS_REMINDERS_WIDGET_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_REMINDERS_WIDGET))
+#define E_REMINDERS_WIDGET_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_REMINDERS_WIDGET, ERemindersWidgetClass))
+
+G_BEGIN_DECLS
+
+enum {
+       E_REMINDERS_WIDGET_COLUMN_OVERDUE,      /* gchar *, markup with time to start/overdue description */
+       E_REMINDERS_WIDGET_COLUMN_DESCRIPTION,  /* gchar *, markup describing the reminder, not component's 
DESCRIPTION property */
+       E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA,/* EReminderData * */
+       E_REMINDERS_WIDGET_N_COLUMNS
+};
+
+typedef struct _ERemindersWidget ERemindersWidget;
+typedef struct _ERemindersWidgetClass ERemindersWidgetClass;
+typedef struct _ERemindersWidgetPrivate ERemindersWidgetPrivate;
+
+/**
+ * ERemindersWidget:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.30
+ **/
+struct _ERemindersWidget {
+       /*< private >*/
+       GtkGrid parent;
+       ERemindersWidgetPrivate *priv;
+};
+
+/**
+ * ERemindersWidgetClass:
+ *
+ * Class structure for the #ERemindersWidget class.
+ *
+ * Since: 3.30
+ **/
+struct _ERemindersWidgetClass {
+       /*< private >*/
+       GtkGridClass parent_class;
+
+       /* Signals and methods */
+       void            (* changed)             (ERemindersWidget *reminders);
+       gboolean        (* activated)           (ERemindersWidget *reminders,
+                                                const EReminderData *rd);
+
+       /* Padding for future expansion */
+       gpointer reserved[10];
+};
+
+GType          e_reminders_widget_get_type     (void) G_GNUC_CONST;
+
+ERemindersWidget *
+               e_reminders_widget_new          (EReminderWatcher *watcher);
+EReminderWatcher *
+               e_reminders_widget_get_watcher  (ERemindersWidget *reminders);
+GSettings *    e_reminders_widget_get_settings (ERemindersWidget *reminders);
+gboolean       e_reminders_widget_is_empty     (ERemindersWidget *reminders);
+GtkTreeView *  e_reminders_widget_get_tree_view(ERemindersWidget *reminders);
+void           e_reminders_widget_report_error (ERemindersWidget *reminders,
+                                                const gchar *prefix,
+                                                const GError *error);
+
+G_END_DECLS
+
+#endif /* E_REMINDERS_WIDGET_H */
diff --git a/src/libedataserverui/libedataserverui-private.c b/src/libedataserverui/libedataserverui-private.c
new file mode 100644
index 0000000..83cb414
--- /dev/null
+++ b/src/libedataserverui/libedataserverui-private.c
@@ -0,0 +1,49 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <glib.h>
+
+#include "libedataserver/libedataserver.h"
+#include "libedataserver/libedataserver-private.h"
+
+#include "libedataserverui-private.h"
+
+/*
+ * _libedataserverui_load_modules:
+ *
+ * Usually called in a GObject::constructed() method to ensure
+ * the modules from the UI module directories are loaded.
+ *
+ * Since: 3.30
+ **/
+void
+_libedataserverui_load_modules (void)
+{
+       static gboolean modules_loaded = FALSE;
+
+       /* Load modules only once. */
+       if (!modules_loaded) {
+               GList *module_types;
+
+               modules_loaded = TRUE;
+
+               module_types = e_module_load_all_in_directory (E_DATA_SERVER_UIMODULEDIR);
+               g_list_free_full (module_types, (GDestroyNotify) g_type_module_unuse);
+       }
+}
diff --git a/src/libedataserverui/libedataserverui-private.h b/src/libedataserverui/libedataserverui-private.h
new file mode 100644
index 0000000..61762bb
--- /dev/null
+++ b/src/libedataserverui/libedataserverui-private.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBEDATASERVERUI_PRIVATE_H
+#define LIBEDATASERVERUI_PRIVATE_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+void           _libedataserverui_load_modules          (void);
+
+G_END_DECLS
+
+#endif /* LIBEDATASERVERUI_PRIVATE_H */
diff --git a/src/libedataserverui/libedataserverui.h b/src/libedataserverui/libedataserverui.h
index 4e3a875..30def63 100644
--- a/src/libedataserverui/libedataserverui.h
+++ b/src/libedataserverui/libedataserverui.h
@@ -25,6 +25,7 @@
 #include <libedataserverui/e-credentials-prompter-impl.h>
 #include <libedataserverui/e-credentials-prompter-impl-oauth2.h>
 #include <libedataserverui/e-credentials-prompter-impl-password.h>
+#include <libedataserverui/e-reminders-widget.h>
 #include <libedataserverui/e-trust-prompt.h>
 #include <libedataserverui/e-webdav-discover-widget.h>
 
diff --git a/src/libedataserverui/libedataserverui.pc.in b/src/libedataserverui/libedataserverui.pc.in
index c3fae5f..1b9747f 100644
--- a/src/libedataserverui/libedataserverui.pc.in
+++ b/src/libedataserverui/libedataserverui.pc.in
@@ -9,11 +9,12 @@ privlibdir=@privlibdir@
 privincludedir=@privincludedir@
 
 credentialmoduledir=@credentialmoduledir@
+uimoduledir=@uimoduledir@
 
 Name: libedataserverui
 Description: UI utility library for Evolution Data Server
 Version: @PROJECT_VERSION@
-Requires: gio-2.0 gmodule-2.0 libsecret-1 libxml-2.0 libsoup-2.4 gtk+-3.0 libedataserver-@API_VERSION@
+Requires: gio-2.0 gmodule-2.0 libsecret-1 libxml-2.0 libsoup-2.4 gtk+-3.0 libedataserver-@API_VERSION@ 
libecal-@API_VERSION@
 Requires.private: camel-@API_VERSION@
 Libs: -L${libdir} -ledataserver-@API_VERSION@ -ledataserverui-@API_VERSION@
 Cflags: -I${privincludedir}
diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt
index a3f1c3f..908d33f 100644
--- a/src/services/CMakeLists.txt
+++ b/src/services/CMakeLists.txt
@@ -3,5 +3,6 @@ add_subdirectory(evolution-calendar-factory)
 add_subdirectory(evolution-source-registry)
 
 if(HAVE_GTK)
+       add_subdirectory(evolution-alarm-notify)
        add_subdirectory(evolution-user-prompter)
 endif(HAVE_GTK)
diff --git a/src/services/evolution-alarm-notify/CMakeLists.txt 
b/src/services/evolution-alarm-notify/CMakeLists.txt
new file mode 100644
index 0000000..a4c0a41
--- /dev/null
+++ b/src/services/evolution-alarm-notify/CMakeLists.txt
@@ -0,0 +1,67 @@
+set(DEPENDENCIES
+       ecal
+       edataserverui
+)
+
+set(SOURCES
+       evolution-alarm-notify.c
+       e-alarm-notify.h
+       e-alarm-notify.c
+)
+
+add_executable(evolution-alarm-notify
+       ${SOURCES}
+)
+
+add_dependencies(evolution-alarm-notify
+       ${DEPENDENCIES}
+)
+
+target_compile_definitions(evolution-alarm-notify PRIVATE
+       -DG_LOG_DOMAIN=\"evolution-alarm-notify\"
+       -DLOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
+)
+
+target_compile_options(evolution-alarm-notify PUBLIC
+       ${CANBERRA_CFLAGS}
+       ${DATA_SERVER_CFLAGS}
+       ${GNOME_PLATFORM_CFLAGS}
+       ${GTK_CFLAGS}
+)
+
+target_include_directories(evolution-alarm-notify PUBLIC
+       ${CMAKE_BINARY_DIR}
+       ${CMAKE_BINARY_DIR}/src
+       ${CMAKE_SOURCE_DIR}/src
+       ${CANBERRA_INCLUDE_DIRS}
+       ${DATA_SERVER_INCLUDE_DIRS}
+       ${GNOME_PLATFORM_INCLUDE_DIRS}
+       ${GTK_INCLUDE_DIRS}
+)
+
+target_link_libraries(evolution-alarm-notify
+       ${DEPENDENCIES}
+       ${CANBERRA_LDFLAGS}
+       ${DATA_SERVER_LDFLAGS}
+       ${GNOME_PLATFORM_LDFLAGS}
+       ${GTK_LDFLAGS}
+)
+
+if(WIN32)
+       find_program(WINDRES windres)
+       if(WINDRES)
+               add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/evolution-alarm-notify-icon.o
+                       COMMAND ${WINDRES} ${CMAKE_CURRENT_SOURCE_DIR}/evolution-alarm-notify-icon.rc 
${CMAKE_CURRENT_BINARY_DIR}/evolution-alarm-notify-icon.o
+                       DEPENDS evolution-alarm-notify-icon.rc
+                               evolution-alarm-notify.ico
+               )
+
+               target_link_libraries(evolution-alarm-notify
+                       ${CMAKE_CURRENT_BINARY_DIR}/evolution-alarm-notify-icon.o
+               )
+       endif(WINDRES)
+endif(WIN32)
+
+install(TARGETS evolution-alarm-notify
+       DESTINATION ${privlibexecdir}
+)
diff --git a/src/services/evolution-alarm-notify/e-alarm-notify.c 
b/src/services/evolution-alarm-notify/e-alarm-notify.c
new file mode 100644
index 0000000..be97043
--- /dev/null
+++ b/src/services/evolution-alarm-notify/e-alarm-notify.c
@@ -0,0 +1,1105 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <time.h>
+
+#ifdef HAVE_CANBERRA
+#include <canberra-gtk.h>
+#endif
+
+#include <glib/gi18n-lib.h>
+
+#ifndef G_OS_WIN32
+#include <gio/gdesktopappinfo.h>
+#endif
+
+#include "libecal/libecal.h"
+#include "libedataserverui/libedataserverui.h"
+
+#include "e-alarm-notify.h"
+
+#define APPLICATION_ID "org.gnome.EvolutionAlarmNotify"
+
+struct _EAlarmNotifyPrivate {
+       ESourceRegistry *registry;
+       EReminderWatcher *watcher;
+       GSettings *settings;
+
+       ERemindersWidget *reminders; /* owned by 'window' */
+       GtkWidget *window;
+       gint window_x;
+       gint window_y;
+       gint window_width;
+       gint window_height;
+       gint window_geometry_save_id;
+
+       GMutex dismiss_lock;
+       GSList *dismiss; /* EReminderData * */
+       GThread *dismiss_thread; /* not referenced, only to know whether it's scheduled */
+
+       GHashTable *notification_ids; /* gchar * ~> NULL, known notifications */
+       GtkStatusIcon *status_icon;
+       gchar *status_icon_tooltip;
+       gint status_icon_blink_id;
+       gint status_icon_blink_countdown;
+       gint last_n_reminders;
+};
+
+/* Forward Declarations */
+static void    e_alarm_notify_initable_init    (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EAlarmNotify, e_alarm_notify, GTK_TYPE_APPLICATION,
+       G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, e_alarm_notify_initable_init))
+
+static void
+e_alarm_notify_show_window (EAlarmNotify *an,
+                           gboolean focus_on_map)
+{
+       GtkWindow *window;
+       gboolean was_visible;
+
+       g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+       window = GTK_WINDOW (an->priv->window);
+
+       gtk_window_set_keep_above (window, g_settings_get_boolean (an->priv->settings, 
"notify-window-on-top"));
+       gtk_window_set_focus_on_map (window, focus_on_map);
+       gtk_window_set_urgency_hint (window, !focus_on_map);
+
+       was_visible = gtk_widget_get_visible (an->priv->window);
+
+       gtk_window_present (window);
+
+       if (!was_visible)
+               gtk_window_move (window, an->priv->window_x, an->priv->window_y);
+}
+
+static gboolean
+e_alarm_notify_audio (EAlarmNotify *an,
+                     const EReminderData *rd,
+                     ECalComponentAlarm *alarm)
+{
+       icalattach *attach = NULL;
+       gboolean did_play = FALSE;
+
+       g_return_val_if_fail (an != NULL, FALSE);
+       g_return_val_if_fail (rd != NULL, FALSE);
+       g_return_val_if_fail (alarm != NULL, FALSE);
+
+       e_cal_component_alarm_get_attach (alarm, &attach);
+
+       if (attach && icalattach_get_is_url (attach)) {
+               const gchar *url;
+
+               url = icalattach_get_url (attach);
+               if (url && *url) {
+                       gchar *filename;
+                       GError *error = NULL;
+
+                       filename = g_filename_from_uri (url, NULL, &error);
+
+                       if (error != NULL) {
+                               g_warning ("%s: Failed to convert URI to filename: %s", G_STRFUNC, 
error->message);
+                               g_error_free (error);
+                       } else if (filename && g_file_test (filename, G_FILE_TEST_EXISTS)) {
+#ifdef HAVE_CANBERRA
+                               did_play = ca_context_play (ca_gtk_context_get (), 0,
+                                       CA_PROP_MEDIA_FILENAME, filename,
+                                       NULL) == 0;
+#endif
+                       }
+
+                       g_free (filename);
+               }
+       }
+
+       if (!did_play)
+               gdk_beep ();
+
+       if (attach)
+               icalattach_unref (attach);
+
+       return FALSE;
+}
+
+/* Copy of e_util_is_running_gnome() from Evolution */
+static gboolean
+e_alarm_notify_is_running_gnome (void)
+{
+#ifdef G_OS_WIN32
+       return FALSE;
+#else
+       static gint runs_gnome = -1;
+
+       if (runs_gnome == -1) {
+               runs_gnome = g_strcmp0 (g_getenv ("XDG_CURRENT_DESKTOP"), "GNOME") == 0 ? 1 : 0;
+               if (runs_gnome) {
+                       GDesktopAppInfo *app_info;
+
+                       app_info = g_desktop_app_info_new ("gnome-notifications-panel.desktop");
+                       if (!app_info) {
+                               runs_gnome = 0;
+                       }
+
+                       g_clear_object (&app_info);
+               }
+       }
+
+       return runs_gnome != 0;
+#endif
+}
+
+static gchar *
+e_alarm_notify_build_notif_id (const EReminderData *rd)
+{
+       GString *string;
+       ECalComponentId *id;
+
+       g_return_val_if_fail (rd != NULL, NULL);
+
+       string = g_string_sized_new (32);
+
+       if (rd->source_uid) {
+               g_string_append (string, rd->source_uid);
+               g_string_append (string, "\n");
+       }
+
+       id = e_cal_component_get_id (rd->component);
+       if (id) {
+               if (id->uid) {
+                       g_string_append (string, id->uid);
+                       g_string_append (string, "\n");
+               }
+
+               if (id->rid) {
+                       g_string_append (string, id->rid);
+                       g_string_append (string, "\n");
+               }
+
+               e_cal_component_free_id (id);
+       }
+
+       g_string_append_printf (string, "%" G_GINT64_FORMAT, rd->instance.trigger);
+
+       return g_string_free (string, FALSE);
+}
+
+static gboolean
+e_alarm_notify_display (EAlarmNotify *an,
+                       const EReminderData *rd,
+                       ECalComponentAlarm *alarm)
+{
+       gchar *description, *notif_id;
+
+       g_return_val_if_fail (an != NULL, FALSE);
+       g_return_val_if_fail (rd != NULL, FALSE);
+       g_return_val_if_fail (alarm != NULL, FALSE);
+
+       description = e_reminder_watcher_describe_data (an->priv->watcher, rd, 
E_REMINDER_WATCHER_DESCRIBE_FLAG_NONE);
+
+       notif_id = e_alarm_notify_build_notif_id (rd);
+
+       if (!g_hash_table_contains (an->priv->notification_ids, notif_id)) {
+               GNotification *notification;
+               GtkIconInfo *icon_info;
+               gchar *detailed_action;
+
+               notification = g_notification_new (_("Reminders"));
+               g_notification_set_body (notification, description);
+
+               icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (), "appointment-soon", 
GTK_ICON_SIZE_DIALOG, 0);
+               if (icon_info) {
+                       const gchar *filename;
+
+                       filename = gtk_icon_info_get_filename (icon_info);
+                       if (filename && *filename) {
+                               GFile *file;
+                               GIcon *icon;
+
+                               file = g_file_new_for_path (filename);
+                               icon = g_file_icon_new (file);
+
+                               if (icon) {
+                                       g_notification_set_icon (notification, icon);
+                                       g_object_unref (icon);
+                               }
+
+                               g_object_unref (file);
+                       }
+
+                       gtk_icon_info_free (icon_info);
+               }
+
+               detailed_action = g_action_print_detailed_name ("app.show-reminders", NULL);
+               g_notification_add_button (notification, _("Reminders"), detailed_action);
+               g_free (detailed_action);
+
+               g_application_send_notification (G_APPLICATION (an), notif_id, notification);
+
+               g_object_unref (notification);
+
+               g_hash_table_insert (an->priv->notification_ids, notif_id, NULL);
+       }
+
+       g_free (an->priv->status_icon_tooltip);
+       an->priv->status_icon_tooltip = description; /* takes ownership */
+
+       if (!g_settings_get_boolean (an->priv->settings, "notify-with-tray"))
+               e_alarm_notify_show_window (an, FALSE);
+
+       return TRUE;
+}
+
+static gboolean
+e_alarm_notify_email (EAlarmNotify *an,
+                     const EReminderData *rd,
+                     ECalComponentAlarm *alarm)
+{
+       ECalClient *client;
+
+       g_return_val_if_fail (an != NULL, FALSE);
+       g_return_val_if_fail (rd != NULL, FALSE);
+       g_return_val_if_fail (alarm != NULL, FALSE);
+
+       client = e_reminder_watcher_ref_opened_client (an->priv->watcher, rd->source_uid);
+       if (client && !e_client_check_capability (E_CLIENT (client), CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS)) {
+               g_object_unref (client);
+               return FALSE;
+       }
+
+       g_clear_object (&client);
+
+       /* Do not know how to send an email from here, but an application can write an extension
+          of E_TYPE_REMINDERS_WIDGET, listen for EReminderWatcher::triggered signal and do what
+          is required from that handler. */
+
+       return FALSE;
+}
+
+static gboolean
+e_alarm_notify_is_blessed_program (GSettings *settings,
+                                  const gchar *url)
+{
+       gchar **list;
+       gint ii;
+       gboolean found = FALSE;
+
+       g_return_val_if_fail (G_IS_SETTINGS (settings), FALSE);
+       g_return_val_if_fail (url != NULL, FALSE);
+
+       list = g_settings_get_strv (settings, "notify-programs");
+
+       for (ii = 0; list && list[ii] && !found; ii++) {
+               found = g_strcmp0 (list[ii], url) == 0;
+       }
+
+       g_strfreev (list);
+
+       return found;
+}
+
+static void
+e_alarm_notify_save_blessed_program (GSettings *settings,
+                                    const gchar *url)
+{
+       gchar **list;
+       gint ii;
+       GPtrArray *array;
+
+       g_return_if_fail (G_IS_SETTINGS (settings));
+       g_return_if_fail (url != NULL);
+
+       array = g_ptr_array_new ();
+
+       list = g_settings_get_strv (settings, "notify-programs");
+
+       for (ii = 0; list && list[ii]; ii++) {
+               if (g_strcmp0 (url, list[ii]) != 0)
+                       g_ptr_array_add (array, list[ii]);
+       }
+
+       g_ptr_array_add (array, (gpointer) url);
+       g_ptr_array_add (array, NULL);
+
+       g_settings_set_strv (settings, "notify-programs", (const gchar * const *) array->pdata);
+
+       g_ptr_array_free (array, TRUE);
+       g_strfreev (list);
+}
+
+static gboolean
+e_alarm_notify_can_procedure (EAlarmNotify *an,
+                             const gchar *cmd,
+                             const gchar *url)
+{
+       GtkWidget *container;
+       GtkWidget *dialog;
+       GtkWidget *label;
+       GtkWidget *checkbox;
+       gchar *str;
+       gint response;
+
+       if (e_alarm_notify_is_blessed_program (an->priv->settings, url))
+               return TRUE;
+
+       dialog = gtk_dialog_new_with_buttons (
+               _("Warning"), GTK_WINDOW (an->priv->window), 0,
+               _("_No"), GTK_RESPONSE_CANCEL,
+               _("_Yes"), GTK_RESPONSE_OK,
+               NULL);
+
+       str = g_strdup_printf (
+               _("A calendar reminder is about to trigger. "
+               "This reminder is configured to run the following program:\n\n"
+               "        %s\n\n"
+               "Are you sure you want to run this program?"),
+               cmd);
+       label = gtk_label_new (str);
+       gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
+       gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_LEFT);
+       gtk_widget_show (label);
+
+       container = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+       gtk_box_pack_start (GTK_BOX (container), label, TRUE, TRUE, 4);
+       g_free (str);
+
+       checkbox = gtk_check_button_new_with_label (_("Do not ask me about this program again"));
+       gtk_widget_show (checkbox);
+       gtk_box_pack_start (GTK_BOX (container), checkbox, TRUE, TRUE, 4);
+
+       response = gtk_dialog_run (GTK_DIALOG (dialog));
+
+       if (response == GTK_RESPONSE_OK &&
+           gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (checkbox))) {
+               e_alarm_notify_save_blessed_program (an->priv->settings, url);
+       }
+
+       gtk_widget_destroy (dialog);
+
+       return response == GTK_RESPONSE_OK;
+}
+
+static gboolean
+e_alarm_notify_procedure (EAlarmNotify *an,
+                         const EReminderData *rd,
+                         ECalComponentAlarm *alarm)
+{
+       ECalComponentText description;
+       icalattach *attach;
+       const gchar *url;
+       gchar *cmd;
+       gboolean result = FALSE;
+
+       g_return_val_if_fail (an != NULL, FALSE);
+       g_return_val_if_fail (rd != NULL, FALSE);
+       g_return_val_if_fail (alarm != NULL, FALSE);
+
+       e_cal_component_alarm_get_attach (alarm, &attach);
+       e_cal_component_alarm_get_description (alarm, &description);
+
+       /* If the alarm has no attachment, simply display a notification dialog. */
+       if (!attach)
+               goto fallback;
+
+       if (!icalattach_get_is_url (attach)) {
+               icalattach_unref (attach);
+               goto fallback;
+       }
+
+       url = icalattach_get_url (attach);
+       g_return_val_if_fail (url != NULL, FALSE);
+
+       /* Ask for confirmation before executing the stuff */
+       if (description.value)
+               cmd = g_strconcat (url, " ", description.value, NULL);
+       else
+               cmd = (gchar *) url;
+
+       if (e_alarm_notify_can_procedure (an, cmd, url))
+               result = g_spawn_command_line_async (cmd, NULL);
+
+       if (cmd != (gchar *) url)
+               g_free (cmd);
+
+       icalattach_unref (attach);
+
+       /* Fall back to display notification if we got an error */
+       if (!result)
+               goto fallback;
+
+       return FALSE;
+
+ fallback:
+       return e_alarm_notify_display (an, rd, alarm);
+}
+
+/* Returns %TRUE to keep in ERemindersWidget */
+static gboolean
+e_alarm_notify_process (EAlarmNotify *an,
+                       const EReminderData *rd,
+                       gboolean snoozed)
+{
+       ECalComponentAlarm *alarm;
+       ECalComponentAlarmAction action;
+       gboolean keep_in_reminders = FALSE;
+
+       g_return_val_if_fail (an != NULL, FALSE);
+       g_return_val_if_fail (rd != NULL, FALSE);
+
+       if (e_cal_component_get_vtype (rd->component) == E_CAL_COMPONENT_TODO) {
+               icalproperty_status status = ICAL_STATUS_NONE;
+
+               e_cal_component_get_status (rd->component, &status);
+
+               if (status == ICAL_STATUS_COMPLETED &&
+                   !g_settings_get_boolean (an->priv->settings, "notify-completed-tasks")) {
+                       return FALSE;
+               }
+       }
+
+       alarm = e_cal_component_get_alarm (rd->component, rd->instance.auid);
+       if (!alarm)
+               return FALSE;
+
+       if (!snoozed && !g_settings_get_boolean (an->priv->settings, "notify-past-events")) {
+               ECalComponentAlarmTrigger trigger;
+               ECalComponentAlarmRepeat repeat;
+               time_t offset = 0, event_relative, orig_trigger_day, today;
+
+               e_cal_component_alarm_get_trigger (alarm, &trigger);
+               e_cal_component_alarm_get_repeat (alarm, &repeat);
+
+               switch (trigger.type) {
+               case E_CAL_COMPONENT_ALARM_TRIGGER_NONE:
+               case E_CAL_COMPONENT_ALARM_TRIGGER_ABSOLUTE:
+                       break;
+
+               case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START:
+               case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_END:
+                       offset = icaldurationtype_as_int (trigger.u.rel_duration);
+                       break;
+
+               default:
+                       break;
+               }
+
+               today = time (NULL);
+               event_relative = rd->instance.occur_start - offset;
+
+               #define CLAMP_TO_DAY(x) ((x) - ((x) % (60 * 60 * 24)))
+
+               event_relative = CLAMP_TO_DAY (event_relative);
+               orig_trigger_day = CLAMP_TO_DAY (rd->instance.trigger);
+               today = CLAMP_TO_DAY (today);
+
+               #undef CLAMP_TO_DAY
+
+               if (event_relative < today && orig_trigger_day < today) {
+                       e_cal_component_alarm_free (alarm);
+                       return FALSE;
+               }
+       }
+
+       e_cal_component_alarm_get_action (alarm, &action);
+
+       switch (action) {
+       case E_CAL_COMPONENT_ALARM_AUDIO:
+               keep_in_reminders = e_alarm_notify_audio (an, rd, alarm);
+               break;
+
+       case E_CAL_COMPONENT_ALARM_DISPLAY:
+               keep_in_reminders = e_alarm_notify_display (an, rd, alarm);
+               break;
+
+       case E_CAL_COMPONENT_ALARM_EMAIL:
+               keep_in_reminders = e_alarm_notify_email (an, rd, alarm);
+               break;
+
+       case E_CAL_COMPONENT_ALARM_PROCEDURE:
+               keep_in_reminders = e_alarm_notify_procedure (an, rd, alarm);
+               break;
+
+       case E_CAL_COMPONENT_ALARM_NONE:
+       case E_CAL_COMPONENT_ALARM_UNKNOWN:
+               break;
+       }
+
+       e_cal_component_alarm_free (alarm);
+
+       return keep_in_reminders;
+}
+
+static gpointer
+e_alarm_notify_dismiss_thread (gpointer user_data)
+{
+       EAlarmNotify *an = user_data;
+       GSList *dismiss, *link;
+
+       g_return_val_if_fail (E_IS_ALARM_NOTIFY (an), NULL);
+
+       g_mutex_lock (&an->priv->dismiss_lock);
+       dismiss = an->priv->dismiss;
+       an->priv->dismiss = NULL;
+       an->priv->dismiss_thread = NULL;
+       g_mutex_unlock (&an->priv->dismiss_lock);
+
+       if (an->priv->watcher) {
+               for (link = dismiss; link; link = g_slist_next (link)) {
+                       EReminderData *rd = link->data;
+
+                       if (rd) {
+                               /* Silently ignore any errors here */
+                               e_reminder_watcher_dismiss_sync (an->priv->watcher, rd, NULL, NULL);
+                       }
+               }
+       }
+
+       g_slist_free_full (dismiss, e_reminder_data_free);
+       g_clear_object (&an);
+
+       return NULL;
+}
+
+static void
+e_alarm_notify_triggered_cb (EReminderWatcher *watcher,
+                            const GSList *reminders, /* EReminderData * */
+                            gboolean snoozed,
+                            gpointer user_data)
+{
+       EAlarmNotify *an = user_data;
+       GSList *link;
+
+       g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+       g_mutex_lock (&an->priv->dismiss_lock);
+
+       for (link = (GSList *) reminders; link; link = g_slist_next (link)) {
+               const EReminderData *rd = link->data;
+
+               if (rd && !e_alarm_notify_process (an, rd, snoozed)) {
+                       an->priv->dismiss = g_slist_prepend (an->priv->dismiss, e_reminder_data_copy (rd));
+               }
+       }
+
+       if (an->priv->dismiss && !an->priv->dismiss_thread) {
+               an->priv->dismiss_thread = g_thread_new (NULL, e_alarm_notify_dismiss_thread, g_object_ref 
(an));
+               g_warn_if_fail (an->priv->dismiss_thread != NULL);
+               if (an->priv->dismiss_thread)
+                       g_thread_unref (an->priv->dismiss_thread);
+       }
+
+       g_mutex_unlock (&an->priv->dismiss_lock);
+}
+
+static void
+e_alarm_notify_status_icon_activated_cb (GtkStatusIcon *status_icon,
+                                        gpointer user_data)
+{
+       EAlarmNotify *an = user_data;
+
+       g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+       if (gtk_widget_get_visible (an->priv->window))
+               gtk_widget_set_visible (an->priv->window, FALSE);
+       else
+               e_alarm_notify_show_window (an, TRUE);
+
+       if (an->priv->status_icon_blink_id > 0) {
+               g_source_remove (an->priv->status_icon_blink_id);
+               an->priv->status_icon_blink_id = -1;
+
+               if (an->priv->status_icon)
+                       gtk_status_icon_set_from_icon_name (an->priv->status_icon, "appointment-soon");
+       }
+}
+
+static gboolean
+e_alarm_notify_popup_destroy_idle_cb (gpointer user_data)
+{
+       GtkWidget *widget = user_data;
+
+       g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE);
+
+       gtk_widget_destroy (widget);
+
+       return FALSE;
+}
+
+static void
+e_alarm_notify_schedule_popup_destroy (GtkWidget *widget)
+{
+       g_idle_add_full (G_PRIORITY_LOW, e_alarm_notify_popup_destroy_idle_cb, widget, NULL);
+}
+
+static void
+e_alarm_notify_status_icon_popup_menu_cb (GtkStatusIcon *status_icon,
+                                         guint button,
+                                         guint activate_time,
+                                         gpointer user_data)
+{
+       struct _items {
+               const gchar *label;
+               const gchar *opt_name;
+       } items[] = {
+               { N_("Display reminders in notification area _only"), "notify-with-tray" },
+               { N_("Keep reminder notification window always on _top"), "notify-window-on-top" },
+               { N_("Display reminders for _completed tasks"), "notify-completed-tasks" },
+               { N_("Display reminders for _past events"), "notify-past-events" }
+       };
+
+       EAlarmNotify *an = user_data;
+       GtkWidget *popup_menu;
+       GtkMenuShell *menu_shell;
+       GtkWidget *item;
+       gint ii;
+
+       g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+       popup_menu = gtk_menu_new ();
+       menu_shell = GTK_MENU_SHELL (popup_menu);
+
+       item = gtk_menu_item_new_with_label (_("Reminders Options:"));
+       gtk_widget_set_sensitive (item, FALSE);
+       gtk_menu_shell_append (menu_shell, item);
+
+       item = gtk_separator_menu_item_new ();
+       gtk_menu_shell_append (menu_shell, item);
+
+       for (ii = 0; ii < G_N_ELEMENTS (items); ii++) {
+               item = gtk_check_menu_item_new_with_mnemonic (_(items[ii].label));
+               gtk_menu_shell_append (menu_shell, item);
+
+               g_settings_bind (an->priv->settings, items[ii].opt_name,
+                       item, "active",
+                       G_SETTINGS_BIND_DEFAULT);
+       }
+
+       g_signal_connect (popup_menu, "deactivate", G_CALLBACK (e_alarm_notify_schedule_popup_destroy), NULL);
+
+       gtk_widget_show_all (popup_menu);
+
+       gtk_menu_popup (GTK_MENU (popup_menu), NULL, NULL, NULL, NULL, button, activate_time);
+}
+
+static gboolean
+e_alarm_notify_status_icon_blink_cb (gpointer user_data)
+{
+       EAlarmNotify *an = user_data;
+       const gchar *icon_name;
+
+       if (g_source_is_destroyed (g_main_current_source ()))
+               return FALSE;
+
+       g_return_val_if_fail (E_IS_ALARM_NOTIFY (an), FALSE);
+
+       an->priv->status_icon_blink_countdown--;
+
+       if (!(an->priv->status_icon_blink_countdown & 1) && an->priv->status_icon_blink_countdown > 0)
+               icon_name = "appointment-missed";
+       else
+               icon_name = "appointment-soon";
+
+       if (an->priv->status_icon)
+               gtk_status_icon_set_from_icon_name (an->priv->status_icon, icon_name);
+
+       if (an->priv->status_icon_blink_countdown <= 0 || !an->priv->status_icon)
+               an->priv->status_icon_blink_id = -1;
+
+       return an->priv->status_icon_blink_id != -1;
+}
+
+static void
+e_alarm_notify_reminders_changed_cb (ERemindersWidget *reminders,
+                                    gpointer user_data)
+{
+       EAlarmNotify *an = user_data;
+       GtkTreeView *tree_view;
+       gint n_reminders = 0;
+
+       g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+       tree_view = e_reminders_widget_get_tree_view (an->priv->reminders);
+       if (tree_view) {
+               GtkTreeModel *model;
+
+               model = gtk_tree_view_get_model (tree_view);
+               n_reminders = gtk_tree_model_iter_n_children (model, NULL);
+       }
+
+       /* This is to update tray icon only, which is not used in GNOME */
+       if (!e_alarm_notify_is_running_gnome ()) {
+               if (n_reminders <= 0) {
+                       if (an->priv->status_icon) {
+                               gtk_status_icon_set_visible (an->priv->status_icon, FALSE);
+                               g_clear_object (&an->priv->status_icon);
+                       }
+               } else {
+                       if (!an->priv->status_icon) {
+                               an->priv->status_icon = gtk_status_icon_new ();
+                               gtk_status_icon_set_title (an->priv->status_icon, _("Reminders"));
+                               gtk_status_icon_set_from_icon_name (an->priv->status_icon, 
"appointment-soon");
+
+                               g_signal_connect (an->priv->status_icon, "activate",
+                                       G_CALLBACK (e_alarm_notify_status_icon_activated_cb), an);
+
+                               g_signal_connect (an->priv->status_icon, "popup-menu",
+                                       G_CALLBACK (e_alarm_notify_status_icon_popup_menu_cb), an);
+                       }
+
+                       if (n_reminders == 1 && an->priv->status_icon_tooltip) {
+                               gtk_status_icon_set_tooltip_text (an->priv->status_icon, 
an->priv->status_icon_tooltip);
+                       } else {
+                               gchar *str;
+
+                               str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE,
+                                       "You have %d reminder", "You have %d reminders",
+                                       n_reminders), n_reminders);
+                               gtk_status_icon_set_tooltip_text (an->priv->status_icon, str);
+                               g_free (str);
+                       }
+
+                       gtk_status_icon_set_visible (an->priv->status_icon, TRUE);
+
+                       if (an->priv->status_icon_blink_id <= 0 &&
+                           an->priv->last_n_reminders < n_reminders) {
+                               an->priv->status_icon_blink_countdown = 30;
+                               an->priv->status_icon_blink_id = e_named_timeout_add (500, 
e_alarm_notify_status_icon_blink_cb, an);
+                       }
+               }
+       }
+
+       an->priv->last_n_reminders = n_reminders;
+
+       g_clear_pointer (&an->priv->status_icon_tooltip, g_free);
+
+       if (n_reminders <= 0 && an->priv->window)
+               gtk_widget_set_visible (an->priv->window, FALSE);
+
+       /* If any reminders were snoozed or dismissed remove their notifications as well */
+       if (g_hash_table_size (an->priv->notification_ids)) {
+               GHashTable *notification_ids;
+
+               notification_ids = an->priv->notification_ids;
+               an->priv->notification_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+               if (n_reminders > 0) {
+                       GSList *past, *link;
+
+                       past = e_reminder_watcher_dup_past (an->priv->watcher);
+                       for (link = past; link; link = g_slist_next (link)) {
+                               EReminderData *rd = link->data;
+                               gchar *notif_id;
+
+                               if (!rd)
+                                       continue;
+
+                               notif_id = e_alarm_notify_build_notif_id (rd);
+                               if (g_hash_table_remove (notification_ids, notif_id))
+                                       g_hash_table_insert (an->priv->notification_ids, notif_id, NULL);
+                               else
+                                       g_free (notif_id);
+                       }
+
+                       g_slist_free_full (past, e_reminder_data_free);
+               }
+
+               if (g_hash_table_size (notification_ids)) {
+                       GApplication *application = G_APPLICATION (an);
+                       GHashTableIter iter;
+                       gpointer key;
+
+                       g_hash_table_iter_init (&iter, notification_ids);
+                       while (g_hash_table_iter_next (&iter, &key, NULL)) {
+                               const gchar *notif_id = key;
+
+                               if (notif_id)
+                                       g_application_withdraw_notification (application, notif_id);
+                       }
+               }
+
+               g_hash_table_destroy (notification_ids);
+       }
+}
+
+static gboolean
+e_alarm_notify_window_geometry_save_cb (gpointer user_data)
+{
+       EAlarmNotify *an = user_data;
+
+       if (g_source_is_destroyed (g_main_current_source ()))
+               return FALSE;
+
+       g_return_val_if_fail (E_IS_ALARM_NOTIFY (an), FALSE);
+
+       an->priv->window_geometry_save_id = 0;
+
+       if (an->priv->settings) {
+               #define set_if_changed(_name, _value) G_STMT_START { \
+                       if (g_settings_get_int (an->priv->settings, _name) != _value) \
+                               g_settings_set_int (an->priv->settings, _name, _value); \
+                       } G_STMT_END
+
+               set_if_changed ("notify-window-x", an->priv->window_x);
+               set_if_changed ("notify-window-y", an->priv->window_y);
+               set_if_changed ("notify-window-width", an->priv->window_width);
+               set_if_changed ("notify-window-height", an->priv->window_height);
+
+               #undef set_if_changed
+       }
+
+       return FALSE;
+}
+
+static gboolean
+e_alarm_notify_window_configure_event_cb (GtkWidget *widget,
+                                         GdkEvent *event,
+                                         gpointer user_data)
+{
+       EAlarmNotify *an = user_data;
+
+       g_return_val_if_fail (E_IS_ALARM_NOTIFY (an), FALSE);
+
+       if (an->priv->window && an->priv->settings && gtk_widget_get_visible (an->priv->window)) {
+               gint pos_x = an->priv->window_x, pos_y = an->priv->window_y;
+               gint width = an->priv->window_width, height = an->priv->window_height;
+
+               gtk_window_get_position (GTK_WINDOW (an->priv->window), &pos_x, &pos_y);
+               gtk_window_get_size (GTK_WINDOW (an->priv->window), &width, &height);
+
+               if (pos_x != an->priv->window_x || pos_y != an->priv->window_y ||
+                   width != an->priv->window_width || height != an->priv->window_height) {
+                       an->priv->window_x = pos_x;
+                       an->priv->window_y = pos_y;
+                       an->priv->window_width = width;
+                       an->priv->window_height = height;
+
+                       if (an->priv->window_geometry_save_id > 0)
+                               g_source_remove (an->priv->window_geometry_save_id);
+
+                       an->priv->window_geometry_save_id = e_named_timeout_add_seconds (1,
+                               e_alarm_notify_window_geometry_save_cb, an);
+               }
+       }
+
+       return FALSE;
+}
+
+static void
+e_alarm_notify_action_activate_cb (GSimpleAction *action,
+                                  GVariant *parameter,
+                                  gpointer user_data)
+{
+       EAlarmNotify *an = user_data;
+       const gchar *name;
+
+       g_return_if_fail (G_IS_ACTION (action));
+       g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+       name = g_action_get_name (G_ACTION (action));
+       g_return_if_fail (name != NULL);
+
+       if (g_str_equal (name, "show-reminders")) {
+               e_alarm_notify_show_window (an, TRUE);
+       } else {
+               g_warning ("%s: Unknown app. action '%s'", G_STRFUNC, name);
+       }
+}
+
+static void
+e_alarm_notify_startup (GApplication *application)
+{
+       const GActionEntry actions[] = {
+               { "show-reminders", e_alarm_notify_action_activate_cb, NULL, NULL, NULL }
+       };
+
+       /* Chain up to parent's method. */
+       G_APPLICATION_CLASS (e_alarm_notify_parent_class)->startup (application);
+
+       /* Keep the application running. */
+       g_application_hold (application);
+
+       g_action_map_add_action_entries (G_ACTION_MAP (application), actions, G_N_ELEMENTS (actions), 
application);
+}
+
+static void
+e_alarm_notify_activate (GApplication *application)
+{
+       EAlarmNotify *an = E_ALARM_NOTIFY (application);
+
+       if (g_application_get_is_remote (application)) {
+               g_application_quit (application);
+               return;
+       }
+
+       g_return_if_fail (an->priv->registry != NULL);
+
+       an->priv->watcher = e_reminder_watcher_new (an->priv->registry);
+       an->priv->reminders = e_reminders_widget_new (an->priv->watcher);
+       an->priv->settings = g_object_ref (e_reminders_widget_get_settings (an->priv->reminders));
+
+       g_object_set (G_OBJECT (an->priv->reminders),
+               "halign", GTK_ALIGN_FILL,
+               "hexpand", TRUE,
+               "valign", GTK_ALIGN_FILL,
+               "vexpand", TRUE,
+               NULL);
+
+       an->priv->window = gtk_application_window_new (GTK_APPLICATION (an));
+       gtk_window_set_title (GTK_WINDOW (an->priv->window), _("Reminders"));
+       gtk_window_set_icon_name (GTK_WINDOW (an->priv->window), "appointment-soon");
+       gtk_window_set_default_size (GTK_WINDOW (an->priv->window),
+               g_settings_get_int (an->priv->settings, "notify-window-width"),
+               g_settings_get_int (an->priv->settings, "notify-window-height"));
+       an->priv->window_x = g_settings_get_int (an->priv->settings, "notify-window-x");
+       an->priv->window_y = g_settings_get_int (an->priv->settings, "notify-window-y");
+
+       gtk_container_add (GTK_CONTAINER (an->priv->window), GTK_WIDGET (an->priv->reminders));
+
+       gtk_window_set_keep_above (GTK_WINDOW (an->priv->window), g_settings_get_boolean (an->priv->settings, 
"notify-window-on-top"));
+
+       g_signal_connect (an->priv->watcher, "triggered",
+               G_CALLBACK (e_alarm_notify_triggered_cb), an);
+
+       g_signal_connect (an->priv->reminders, "changed",
+               G_CALLBACK (e_alarm_notify_reminders_changed_cb), an);
+
+       g_signal_connect (an->priv->window, "configure-event",
+               G_CALLBACK (e_alarm_notify_window_configure_event_cb), an);
+
+       g_signal_connect (an->priv->window, "delete-event",
+               G_CALLBACK (gtk_widget_hide_on_delete), an);
+}
+
+static gboolean
+e_alarm_notify_initable (GInitable *initable,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       EAlarmNotify *an = E_ALARM_NOTIFY (initable);
+
+       an->priv->registry = e_source_registry_new_sync (cancellable, error);
+
+       return an->priv->registry != NULL;
+}
+
+static void
+e_alarm_notify_dispose (GObject *object)
+{
+       EAlarmNotify *an = E_ALARM_NOTIFY (object);
+
+       if (an->priv->watcher)
+               g_signal_handlers_disconnect_by_data (an->priv->watcher, an);
+
+       if (an->priv->reminders)
+               g_signal_handlers_disconnect_by_data (an->priv->reminders, an);
+
+       if (an->priv->status_icon_blink_id > 0) {
+               g_source_remove (an->priv->status_icon_blink_id);
+               an->priv->status_icon_blink_id = -1;
+       }
+
+       if (an->priv->window_geometry_save_id > 0) {
+               g_source_remove (an->priv->window_geometry_save_id);
+               an->priv->window_geometry_save_id = 0;
+       }
+
+       if (an->priv->status_icon) {
+               gtk_status_icon_set_visible (an->priv->status_icon, FALSE);
+               g_clear_object (&an->priv->status_icon);
+       }
+
+       if (an->priv->window) {
+               g_signal_handlers_disconnect_by_data (an->priv->window, an);
+
+               gtk_widget_destroy (an->priv->window);
+               an->priv->window = NULL;
+               an->priv->reminders = NULL;
+       }
+
+       g_clear_object (&an->priv->registry);
+       g_clear_object (&an->priv->watcher);
+       g_clear_object (&an->priv->settings);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_alarm_notify_parent_class)->dispose (object);
+}
+
+static void
+e_alarm_notify_finalize (GObject *object)
+{
+       EAlarmNotify *an = E_ALARM_NOTIFY (object);
+
+       g_free (an->priv->status_icon_tooltip);
+       g_mutex_clear (&an->priv->dismiss_lock);
+       g_slist_free_full (an->priv->dismiss, e_reminder_data_free);
+       g_hash_table_destroy (an->priv->notification_ids);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_alarm_notify_parent_class)->finalize (object);
+}
+
+static void
+e_alarm_notify_class_init (EAlarmNotifyClass *klass)
+{
+       GObjectClass *object_class;
+       GApplicationClass *application_class;
+
+       g_type_class_add_private (klass, sizeof (EAlarmNotifyPrivate));
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->dispose = e_alarm_notify_dispose;
+       object_class->finalize = e_alarm_notify_finalize;
+
+       application_class = G_APPLICATION_CLASS (klass);
+       application_class->startup = e_alarm_notify_startup;
+       application_class->activate = e_alarm_notify_activate;
+}
+
+static void
+e_alarm_notify_initable_init (GInitableIface *iface)
+{
+       iface->init = e_alarm_notify_initable;
+}
+
+static void
+e_alarm_notify_init (EAlarmNotify *an)
+{
+       an->priv = G_TYPE_INSTANCE_GET_PRIVATE (an, E_TYPE_ALARM_NOTIFY, EAlarmNotifyPrivate);
+       an->priv->notification_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+       an->priv->last_n_reminders = G_MAXINT32;
+
+       g_mutex_init (&an->priv->dismiss_lock);
+}
+
+/*
+ * e_alarm_notify_new:
+ *
+ * Creates a new #EAlarmNotify object.
+ *
+ * Returns: (transfer full): a newly-created #EAlarmNotify
+ **/
+EAlarmNotify *
+e_alarm_notify_new (GCancellable *cancellable,
+                   GError **error)
+{
+       return g_initable_new (
+               E_TYPE_ALARM_NOTIFY, cancellable, error,
+               "application-id", APPLICATION_ID,
+               NULL);
+}
diff --git a/src/services/evolution-alarm-notify/e-alarm-notify.h 
b/src/services/evolution-alarm-notify/e-alarm-notify.h
new file mode 100644
index 0000000..8c77bc1
--- /dev/null
+++ b/src/services/evolution-alarm-notify/e-alarm-notify.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef E_ALARM_NOTIFY_H
+#define E_ALARM_NOTIFY_H
+
+#include <gtk/gtk.h>
+#include <libecal/libecal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_ALARM_NOTIFY \
+       (e_alarm_notify_get_type ())
+#define E_ALARM_NOTIFY(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_ALARM_NOTIFY, EAlarmNotify))
+#define E_ALARM_NOTIFY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_ALARM_NOTIFY, EAlarmNotifyClass))
+#define E_IS_ALARM_NOTIFY(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_ALARM_NOTIFY))
+#define E_IS_ALARM_NOTIFY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_ALARM_NOTIFY))
+#define E_ALARM_NOTIFY_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_ALARM_NOTIFY, EAlarmNotifyClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EAlarmNotify EAlarmNotify;
+typedef struct _EAlarmNotifyClass EAlarmNotifyClass;
+typedef struct _EAlarmNotifyPrivate EAlarmNotifyPrivate;
+
+struct _EAlarmNotify {
+       /*< private >*/
+       GtkApplication parent;
+       EAlarmNotifyPrivate *priv;
+};
+
+struct _EAlarmNotifyClass {
+       /*< private >*/
+       GtkApplicationClass parent_class;
+};
+
+GType          e_alarm_notify_get_type         (void);
+EAlarmNotify * e_alarm_notify_new              (GCancellable *cancellable,
+                                                GError **error);
+
+G_END_DECLS
+
+#endif /* E_ALARM_NOTIFY_H */
diff --git a/src/services/evolution-alarm-notify/evolution-alarm-notify-icon.rc 
b/src/services/evolution-alarm-notify/evolution-alarm-notify-icon.rc
new file mode 100644
index 0000000..1f9ef65
--- /dev/null
+++ b/src/services/evolution-alarm-notify/evolution-alarm-notify-icon.rc
@@ -0,0 +1 @@
+1      ICON    "evolution-alarm-notify.ico"
diff --git a/src/services/evolution-alarm-notify/evolution-alarm-notify.c 
b/src/services/evolution-alarm-notify/evolution-alarm-notify.c
new file mode 100644
index 0000000..d3a46ba
--- /dev/null
+++ b/src/services/evolution-alarm-notify/evolution-alarm-notify.c
@@ -0,0 +1,106 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <locale.h>
+#include <libintl.h>
+#include <glib/gi18n.h>
+
+#include <libedataserver/libedataserver.h>
+#include <libedataserverui/libedataserverui.h>
+
+#include "e-alarm-notify.h"
+
+#ifdef G_OS_UNIX
+#include <glib-unix.h>
+
+static gboolean
+handle_term_signal (gpointer data)
+{
+       g_application_quit (data);
+
+       return FALSE;
+}
+#endif
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       EAlarmNotify *alarm_notify;
+       gint exit_status;
+       GError *error = NULL;
+
+#ifdef G_OS_WIN32
+       e_util_win32_initialize ();
+#endif
+
+       setlocale (LC_ALL, "");
+       bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+       bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+       textdomain (GETTEXT_PACKAGE);
+
+       /* Workaround https://bugzilla.gnome.org/show_bug.cgi?id=674885 */
+       g_type_ensure (G_TYPE_DBUS_CONNECTION);
+       g_type_ensure (G_TYPE_DBUS_PROXY);
+       g_type_ensure (G_BUS_TYPE_SESSION);
+
+       gtk_init (&argc, &argv);
+
+       if (error != NULL) {
+               g_printerr ("%s\n", error->message);
+               exit (EXIT_FAILURE);
+       }
+
+       e_gdbus_templates_init_main_thread ();
+       e_xml_initialize_in_main ();
+
+       alarm_notify = e_alarm_notify_new (NULL, &error);
+
+       if (error != NULL) {
+               g_printerr ("%s\n", error->message);
+               g_error_free (error);
+               exit (EXIT_FAILURE);
+       }
+
+       g_application_register (G_APPLICATION (alarm_notify), NULL, &error);
+
+       if (error != NULL) {
+               g_printerr ("%s\n", error->message);
+               g_error_free (error);
+               g_object_unref (alarm_notify);
+               exit (EXIT_FAILURE);
+       }
+
+       if (g_application_get_is_remote (G_APPLICATION (alarm_notify))) {
+               g_object_unref (alarm_notify);
+               return 0;
+       }
+
+#ifdef G_OS_UNIX
+       g_unix_signal_add_full (
+               G_PRIORITY_DEFAULT, SIGTERM,
+               handle_term_signal, alarm_notify, NULL);
+#endif
+
+       exit_status = g_application_run (G_APPLICATION (alarm_notify), argc, argv);
+
+       g_object_unref (alarm_notify);
+
+       return exit_status;
+}
diff --git a/src/services/evolution-alarm-notify/evolution-alarm-notify.ico 
b/src/services/evolution-alarm-notify/evolution-alarm-notify.ico
new file mode 100644
index 0000000..6585452
Binary files /dev/null and b/src/services/evolution-alarm-notify/evolution-alarm-notify.ico differ


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