[gnome-calendar/gbsneto/event-preview-popover: 2/2] Introduce GcalEventPopover




commit 68160348dcce3bfda2facc56a3734048946cdd96
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date:   Wed Nov 11 17:04:30 2020 -0300

    Introduce GcalEventPopover
    
    GcalEventPopover gives a preview of the event, and handy access
    to the event description and information. It also allows easily
    joining meetings if any one if available.
    
    The list of supported meeting URLs is small now, but I hope
    more people will contribute as we go.
    
    It is also possible to edit the event through an edit button.

 src/gui/gcal-event-popover.c  | 676 ++++++++++++++++++++++++++++++++++++++++++
 src/gui/gcal-event-popover.h  |  36 +++
 src/gui/gcal-event-popover.ui | 169 +++++++++++
 src/gui/gcal-event-widget.c   |  77 +++++
 src/gui/gcal-event-widget.h   |  15 +-
 src/gui/gcal-meeting-row.c    | 208 +++++++++++++
 src/gui/gcal-meeting-row.h    |  33 +++
 src/gui/gcal-meeting-row.ui   |  78 +++++
 src/gui/gcal-window.c         |  29 +-
 src/gui/gui.gresource.xml     |   2 +
 src/gui/meson.build           |   2 +
 src/theme/Adwaita.css         |  19 ++
 12 files changed, 1337 insertions(+), 7 deletions(-)
---
diff --git a/src/gui/gcal-event-popover.c b/src/gui/gcal-event-popover.c
new file mode 100644
index 00000000..94cfabcf
--- /dev/null
+++ b/src/gui/gcal-event-popover.c
@@ -0,0 +1,676 @@
+/* gcal-event-popover.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "GcalEventPopover"
+
+#include "gcal-debug.h"
+#include "gcal-event-popover.h"
+#include "gcal-meeting-row.h"
+#include "gcal-utils.h"
+
+#include <glib/gi18n.h>
+#include <handy.h>
+
+struct _GcalEventPopover
+{
+  GtkPopover          parent;
+
+  GtkLabel           *date_time_label;
+  GtkLabel           *description_label;
+  GtkWidget          *edit_button;
+  GtkWidget          *location_box;
+  GtkLabel           *location_label;
+  GtkListBox         *meetings_listbox;
+  GtkLabel           *placeholder_label;
+  GtkLabel           *summary_label;
+
+  GcalContext        *context;
+  GcalEvent          *event;
+};
+
+static void          on_join_meeting_cb                          (GcalMeetingRow     *meeting_row,
+                                                                  const gchar        *url,
+                                                                  GcalEventPopover   *self);
+
+G_DEFINE_TYPE (GcalEventPopover, gcal_event_popover, GTK_TYPE_POPOVER)
+
+enum
+{
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_EVENT,
+  N_PROPS
+};
+
+enum
+{
+  EDIT,
+  NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+
+/*
+ * Auxiliary methods
+ */
+
+static gint
+get_number_of_days_from_today (GDateTime *now,
+                               GDateTime *day)
+{
+  GDate event_day;
+  GDate today;
+
+  g_date_set_dmy (&today,
+                  g_date_time_get_day_of_month (now),
+                  g_date_time_get_month (now),
+                  g_date_time_get_year (now));
+
+  g_date_set_dmy (&event_day,
+                  g_date_time_get_day_of_month (day),
+                  g_date_time_get_month (day),
+                  g_date_time_get_year (day));
+
+  return g_date_days_between (&today, &event_day);
+}
+
+static gchar*
+format_time (GcalEventPopover *self,
+             GDateTime        *date)
+{
+  GcalTimeFormat time_format;
+
+  time_format = gcal_context_get_time_format (self->context);
+
+  switch (time_format)
+    {
+    case GCAL_TIME_FORMAT_12H:
+      return g_date_time_format (date, "%I:%M %P");
+
+    case GCAL_TIME_FORMAT_24H:
+      return g_date_time_format (date, "%R");
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  return NULL;
+}
+
+static const gchar*
+get_month_name (gint month)
+{
+  const gchar *month_names[] = {
+    N_("January"),
+    N_("February"),
+    N_("March"),
+    N_("April"),
+    N_("May"),
+    N_("June"),
+    N_("July"),
+    N_("August"),
+    N_("September"),
+    N_("October"),
+    N_("November"),
+    N_("December"),
+    NULL
+  };
+
+  return gettext (month_names[month]);
+}
+
+static gchar*
+format_multiday_date (GcalEventPopover *self,
+                      GDateTime        *dt,
+                      gboolean          force_show_year,
+                      gboolean          show_time)
+{
+  g_autoptr (GDateTime) now = NULL;
+  gint n_days_from_dt;
+
+  now = g_date_time_new_now_local ();
+  n_days_from_dt = get_number_of_days_from_today (now, dt);
+
+  if (show_time)
+    {
+      g_autofree gchar *hours = format_time (self, dt);
+
+      if (n_days_from_dt == 0)
+        {
+          return g_strdup_printf (_("Today %s"), hours);
+        }
+      else if (n_days_from_dt == 1)
+        {
+          return g_strdup_printf (_("Tomorrow %s"), hours);
+        }
+      else if (n_days_from_dt == -1)
+        {
+          return g_strdup_printf (_("Yesterday %s"), hours);
+        }
+      else if (!force_show_year && g_date_time_get_year (now) == g_date_time_get_year (dt))
+        {
+          /*
+           * Translators: %1$s is a month name (e.g. November), %2$d is the day
+           * of month, and %3$ is the hour. This format string results in dates
+           * like "November 21, 22:00".
+           */
+          return g_strdup_printf (_("%1$s %2$d, %3$s"),
+                                  get_month_name (g_date_time_get_month (dt) - 1),
+                                  g_date_time_get_day_of_month (dt),
+                                  hours);
+        }
+      else
+        {
+          /*
+           * Translators: %1$s is a month name (e.g. November), %2$d is the day
+           * of month, %3$d is the year, and %4$s is the hour. This format string
+           * results in dates like "November 21, 2020, 22:00".
+           */
+          return g_strdup_printf (_("%1$s %2$d, %3$d, %4$s"),
+                                  get_month_name (g_date_time_get_month (dt) - 1),
+                                  g_date_time_get_day_of_month (dt),
+                                  g_date_time_get_year (dt),
+                                  hours);
+        }
+
+    }
+  else
+    {
+      if (n_days_from_dt == 0)
+        {
+          return g_strdup (_("Today"));
+        }
+      else if (n_days_from_dt == 1)
+        {
+          return g_strdup (_("Tomorrow"));
+        }
+      else if (n_days_from_dt == -1)
+        {
+          return g_strdup (_("Yesterday"));
+        }
+      else if (!force_show_year && g_date_time_get_year (now) == g_date_time_get_year (dt))
+        {
+          /*
+           * Translators: %1$s is a month name (e.g. November), and %2$d is
+           * the day of month. This format string results in dates like
+           * "November 21".
+           */
+          return g_strdup_printf (_("%1$s %2$d"),
+                                  get_month_name (g_date_time_get_month (dt) - 1),
+                                  g_date_time_get_day_of_month (dt));
+        }
+      else
+        {
+          /*
+           * Translators: %1$s is a month name (e.g. November), %2$d is the day
+           * of month, and %3$d is the year. This format string results in dates
+           * like "November 21, 2020".
+           */
+          return g_strdup_printf (_("%1$s %2$d, %3$d"),
+                                  get_month_name (g_date_time_get_month (dt) - 1),
+                                  g_date_time_get_day_of_month (dt),
+                                  g_date_time_get_year (dt));
+        }
+
+    }
+
+  g_assert_not_reached ();
+}
+
+static gchar*
+format_single_day (GcalEventPopover *self,
+                   GDateTime        *start_dt,
+                   GDateTime        *end_dt,
+                   gboolean          show_time)
+{
+  g_autoptr (GDateTime) now = NULL;
+  gint n_days_from_dt;
+
+  now = g_date_time_new_now_local ();
+  n_days_from_dt = get_number_of_days_from_today (now, start_dt);
+
+  if (show_time)
+    {
+      g_autofree gchar *start_hours = format_time (self, start_dt);
+      g_autofree gchar *end_hours = format_time (self, end_dt);
+
+      if (n_days_from_dt == 0)
+        {
+          /*
+           * Translators: %1$s is the start hour, and %2$s is the end hour, for
+           * example: "Today, 19:00 — 22:00"
+           */
+          return g_strdup_printf (_("Today, %1$s — %2$s"), start_hours, end_hours);
+        }
+      else if (n_days_from_dt == 1)
+        {
+          /*
+           * Translators: %1$s is the start hour, and %2$s is the end hour, for
+           * example: "Tomorrow, 19:00 — 22:00"
+           */
+          return g_strdup_printf (_("Tomorrow, %1$s – %2$s"), start_hours, end_hours);
+        }
+      else if (n_days_from_dt == -1)
+        {
+          /*
+           * Translators: %1$s is the start hour, and %2$s is the end hour, for
+           * example: "Tomorrow, 19:00 — 22:00"
+           */
+          return g_strdup_printf (_("Yesterday, %1$s – %2$s"), start_hours, end_hours);
+        }
+      else if (g_date_time_get_year (now) == g_date_time_get_year (start_dt))
+        {
+          /*
+           * Translators: %1$s is a month name (e.g. November), %2$d is the day
+           * of month, %3$s is the start hour, and %4$s is the end hour. This
+           * format string results in dates like "November 21, 19:00 — 22:00".
+           */
+          return g_strdup_printf (_("%1$s %2$d, %3$s – %4$s"),
+                                  get_month_name (g_date_time_get_month (start_dt) - 1),
+                                  g_date_time_get_day_of_month (start_dt),
+                                  start_hours,
+                                  end_hours);
+        }
+      else
+        {
+          /*
+           * Translators: %1$s is a month name (e.g. November), %2$d is the day
+           * of month, %3$d is the year, %4$s is the start hour, and %5$s is the
+           * end hour. This format string results in dates like:
+           *
+           * "November 21, 2021, 19:00 — 22:00".
+           */
+          return g_strdup_printf (_("%1$s %2$d, %3$d, %4$s – %5$s"),
+                                  get_month_name (g_date_time_get_month (start_dt) - 1),
+                                  g_date_time_get_day_of_month (start_dt),
+                                  g_date_time_get_year (start_dt),
+                                  start_hours,
+                                  end_hours);
+        }
+    }
+  else
+    {
+      if (n_days_from_dt == 0)
+        {
+          return g_strdup (_("Today"));
+        }
+      else if (n_days_from_dt == 1)
+        {
+          return g_strdup (_("Tomorrow"));
+        }
+      else if (n_days_from_dt == -1)
+        {
+          return g_strdup (_("Yesterday"));
+        }
+      else if (g_date_time_get_year (now) == g_date_time_get_year (start_dt))
+        {
+          /*
+           * Translators: %1$s is a month name (e.g. November), and %2$d is
+           * the day of month. This format string results in dates like
+           * "November 21".
+           */
+          return g_strdup_printf (_("%1$s %2$d"),
+                                  get_month_name (g_date_time_get_month (start_dt) - 1),
+                                  g_date_time_get_day_of_month (start_dt));
+        }
+      else
+        {
+          /*
+           * Translators: %1$s is a month name (e.g. November), %2$d is the day
+           * of month, and %3$d is the year. This format string results in dates
+           * like "November 21, 2020".
+           */
+          return g_strdup_printf (_("%1$s %2$d, %3$d"),
+                                  get_month_name (g_date_time_get_month (start_dt) - 1),
+                                  g_date_time_get_day_of_month (start_dt),
+                                  g_date_time_get_year (start_dt));
+        }
+    }
+
+  g_assert_not_reached ();
+}
+
+static void
+update_date_time_label (GcalEventPopover *self)
+{
+  g_autoptr (GString) string = NULL;
+  GDateTime *end_dt;
+  GDateTime *start_dt;
+  gboolean show_hours;
+  gboolean multiday;
+  gboolean all_day;
+
+  string = g_string_new ("");
+  all_day = gcal_event_get_all_day (self->event);
+  multiday = gcal_event_is_multiday (self->event);
+  show_hours = !all_day;
+
+  end_dt = gcal_event_get_date_end (self->event);
+  start_dt = gcal_event_get_date_start (self->event);
+
+  if (multiday)
+    {
+      g_autofree gchar *start_str = NULL;
+      g_autofree gchar *end_str = NULL;
+      gboolean show_year;
+
+      show_year = g_date_time_get_year (start_dt) != g_date_time_get_year (end_dt);
+      start_str = format_multiday_date (self, start_dt, show_year, show_hours);
+      end_str = format_multiday_date (self, end_dt, show_year, show_hours);
+
+      /* Translators: %1$s is the start date, and %2$s. For example: June 21 - November 29, 2022 */
+      g_string_printf (string, _("%1$s — %2$s"), start_str, end_str);
+
+    }
+  else
+    {
+      g_autofree gchar *str = format_single_day (self, start_dt, end_dt, show_hours);
+      g_string_append (string, str);
+    }
+
+  gtk_label_set_label (self->date_time_label, string->str);
+}
+
+static void
+update_placeholder_label (GcalEventPopover *self)
+{
+  gboolean placeholder_visible = FALSE;
+
+  placeholder_visible |= !gtk_widget_get_visible (GTK_WIDGET (self->location_box)) &&
+                         !gtk_widget_get_visible (GTK_WIDGET (self->description_label));
+  gtk_widget_set_visible (GTK_WIDGET (self->placeholder_label), placeholder_visible);
+}
+
+static void
+add_meeting (GcalEventPopover *self,
+             const gchar      *url)
+{
+  GtkWidget *row;
+
+  row = gcal_meeting_row_new (url);
+  g_signal_connect (row, "join-meeting", G_CALLBACK (on_join_meeting_cb), self);
+  gtk_container_add (GTK_CONTAINER (self->meetings_listbox), row);
+
+  gtk_widget_show (GTK_WIDGET (self->meetings_listbox));
+}
+
+static void
+setup_location_label (GcalEventPopover *self)
+{
+  g_autoptr (SoupURI) soup_uri = NULL;
+  g_autofree gchar *location = NULL;
+
+  location = g_strdup (gcal_event_get_location (self->event));
+  g_strstrip (location);
+
+  soup_uri = soup_uri_new (location);
+  if (soup_uri)
+    {
+      GString *string;
+
+      string = g_string_new (NULL);
+      g_string_append (string, "<a href=\"");
+      g_string_append (string, location);
+      g_string_append (string, "\">");
+      g_string_append (string, location);
+      g_string_append (string, "</a>");
+
+      add_meeting (self, location);
+
+      g_clear_pointer (&location, g_free);
+      location = g_string_free (string, FALSE);
+    }
+
+  gtk_widget_set_visible (self->location_box,
+                          location && g_utf8_strlen (location, -1) > 0);
+  gtk_label_set_markup (self->location_label, location);
+}
+
+static void
+setup_description_label (GcalEventPopover *self)
+{
+  g_autofree gchar *description = NULL;
+  g_autofree gchar *meeting_url = NULL;
+  g_autoptr (GString) string = NULL;
+
+  gcal_utils_extract_google_section (gcal_event_get_description (self->event),
+                                     &description,
+                                     &meeting_url);
+  g_strstrip (description);
+
+  if (meeting_url)
+    add_meeting (self, meeting_url);
+
+  string = g_string_new (description);
+  g_string_replace (string, "<br>", "\n", 0);
+  g_string_replace (string, "&nbsp;", " ", 0);
+
+  gtk_label_set_markup (self->description_label, string->str);
+  gtk_widget_set_visible (GTK_WIDGET (self->description_label), string->str && *string->str);
+}
+
+static void
+set_event_internal (GcalEventPopover *self,
+                    GcalEvent        *event)
+{
+  g_set_object (&self->event, event);
+
+  gtk_label_set_label (self->summary_label, gcal_event_get_summary (event));
+
+  setup_description_label (self);
+  setup_location_label (self);
+  update_placeholder_label (self);
+  update_date_time_label (self);
+
+  gtk_widget_grab_focus (self->edit_button);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_edit_button_clicked_cb (GtkButton        *edit_button,
+                           GcalEventPopover *self)
+{
+  g_signal_emit (self, signals[EDIT], 0);
+  gtk_popover_popdown (GTK_POPOVER (self));
+}
+
+static void
+on_join_meeting_cb (GcalMeetingRow   *meeting_row,
+                    const gchar      *url,
+                    GcalEventPopover *self)
+{
+  g_autoptr (GError) error = NULL;
+  GtkWindow *window;
+
+  window = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+  g_assert (window != NULL);
+
+  gtk_show_uri_on_window (window, url, GDK_CURRENT_TIME, &error);
+  if (error)
+    {
+      g_warning ("Error opening URL: %s", error->message);
+      return;
+    }
+
+  /* For some reason, gtk_popover_popdown() crashes when called here */
+  gtk_widget_hide (GTK_WIDGET (self));
+}
+
+static void
+on_time_format_changed_cb (GcalEventPopover *self)
+{
+  GCAL_ENTRY;
+
+  update_date_time_label (self);
+
+  GCAL_EXIT;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gcal_event_popover_finalize (GObject *object)
+{
+  GcalEventPopover *self = (GcalEventPopover *)object;
+
+  g_clear_object (&self->context);
+  g_clear_object (&self->event);
+
+  G_OBJECT_CLASS (gcal_event_popover_parent_class)->finalize (object);
+}
+
+static void
+gcal_event_popover_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  GcalEventPopover *self = GCAL_EVENT_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, self->context);
+      break;
+
+    case PROP_EVENT:
+      g_value_set_object (value, self->event);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_event_popover_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  GcalEventPopover *self = GCAL_EVENT_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_assert (self->context == NULL);
+      self->context = g_value_dup_object (value);
+      g_signal_connect_object (self->context,
+                               "notify::time-format",
+                               G_CALLBACK (on_time_format_changed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+      break;
+
+    case PROP_EVENT:
+      g_assert (self->event == NULL);
+      set_event_internal (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_event_popover_class_init (GcalEventPopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gcal_event_popover_finalize;
+  object_class->get_property = gcal_event_popover_get_property;
+  object_class->set_property = gcal_event_popover_set_property;
+
+  /**
+   * GcalEventPopover::context:
+   *
+   * The context of the event popover.
+   */
+  properties[PROP_CONTEXT] = g_param_spec_object ("context",
+                                                  "Context",
+                                                  "Context",
+                                                  GCAL_TYPE_CONTEXT,
+                                                  G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  /**
+   * GcalEventPopover::event:
+   *
+   * The event this popover represents.
+   */
+  properties[PROP_EVENT] = g_param_spec_object ("event",
+                                                "Event",
+                                                "The event this popover represents",
+                                                GCAL_TYPE_EVENT,
+                                                G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals[EDIT] = g_signal_new ("edit",
+                                GCAL_TYPE_EVENT_POPOVER,
+                                G_SIGNAL_RUN_LAST,
+                                0,
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE,
+                                0);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/calendar/ui/gui/gcal-event-popover.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, date_time_label);
+  gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, description_label);
+  gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, edit_button);
+  gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, location_box);
+  gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, location_label);
+  gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, meetings_listbox);
+  gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, placeholder_label);
+  gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, summary_label);
+
+  gtk_widget_class_bind_template_callback (widget_class, on_edit_button_clicked_cb);
+}
+
+static void
+gcal_event_popover_init (GcalEventPopover *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+    gtk_popover_set_position (GTK_POPOVER (self), GTK_POS_LEFT);
+  else
+    gtk_popover_set_position (GTK_POPOVER (self), GTK_POS_RIGHT);
+}
+
+GtkWidget*
+gcal_event_popover_new (GcalContext *context,
+                        GcalEvent   *event)
+{
+  return g_object_new (GCAL_TYPE_EVENT_POPOVER,
+                       "context", context,
+                       "event", event,
+                       NULL);
+}
diff --git a/src/gui/gcal-event-popover.h b/src/gui/gcal-event-popover.h
new file mode 100644
index 00000000..03f3bc7e
--- /dev/null
+++ b/src/gui/gcal-event-popover.h
@@ -0,0 +1,36 @@
+/* gcal-event-popover.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "gcal-context.h"
+#include "gcal-event.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_EVENT_POPOVER (gcal_event_popover_get_type())
+G_DECLARE_FINAL_TYPE (GcalEventPopover, gcal_event_popover, GCAL, EVENT_POPOVER, GtkPopover)
+
+GtkWidget*           gcal_event_popover_new                      (GcalContext        *context,
+                                                                  GcalEvent          *event);
+
+G_END_DECLS
diff --git a/src/gui/gcal-event-popover.ui b/src/gui/gcal-event-popover.ui
new file mode 100644
index 00000000..3f366e28
--- /dev/null
+++ b/src/gui/gcal-event-popover.ui
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GcalEventPopover" parent="GtkPopover">
+
+    <style>
+      <class name="event-popover" />
+    </style>
+
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="spacing">0</property>
+            <property name="orientation">vertical</property>
+
+            <!-- Title -->
+            <child>
+              <object class="GtkLabel" id="summary_label">
+                <property name="visible">True</property>
+                <property name="margin">12</property>
+                <property name="ellipsize">end</property>
+                <property name="width-chars">30</property>
+                <property name="max-width-chars">40</property>
+                <attributes>
+                  <attribute name="weight" value="bold" />
+                </attributes>
+              </object>
+            </child>
+
+            <!-- Date & Time -->
+            <child>
+              <object class="GtkLabel" id="date_time_label">
+                <property name="visible">True</property>
+                <property name="ellipsize">end</property>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkSeparator">
+                <property name="visible">True</property>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="visible">True</property>
+                <property name="hscrollbar-policy">never</property>
+                <property name="propagate-natural-width">True</property>
+                <property name="propagate-natural-height">True</property>
+                <property name="max-content-height">400</property>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="margin">12</property>
+                    <property name="spacing">12</property>
+                    <property name="orientation">vertical</property>
+
+
+                    <!-- Placeholder -->
+                    <child>
+                      <object class="GtkLabel" id="placeholder_label">
+                        <property name="visible">True</property>
+                        <property name="expand">True</property>
+                        <property name="valign">center</property>
+                        <property name="label" translatable="yes">No event information</property>
+                        <attributes>
+                          <attribute name="style" value="italic" />
+                        </attributes>
+                        <style>
+                          <class name="dim-label" />
+                        </style>
+                      </object>
+                    </child>
+
+                    <!-- Description -->
+                    <child>
+                      <object class="GtkLabel" id="description_label">
+                        <property name="use-markup">True</property>
+                        <property name="selectable">True</property>
+                        <property name="max-width-chars">50</property>
+                        <property name="xalign">0.0</property>
+                        <property name="wrap">True</property>
+                        <property name="wrap-mode">word-char</property>
+                        <style>
+                          <class name="dim-label" />
+                        </style>
+                      </object>
+                    </child>
+
+                    <!-- Location -->
+                    <child>
+                      <object class="GtkBox" id="location_box">
+                        <property name="orientation">vertical</property>
+
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="xalign">0.0</property>
+                            <property name="label" translatable="yes">Location</property>
+                            <attributes>
+                              <attribute name="weight" value="bold" />
+                            </attributes>
+                          </object>
+                        </child>
+
+                        <child>
+                          <object class="GtkSeparator">
+                            <property name="visible">True</property>
+                          </object>
+                        </child>
+
+                        <child>
+                          <object class="GtkLabel" id="location_label">
+                            <property name="visible">True</property>
+                            <property name="selectable">True</property>
+                            <property name="xalign">0.0</property>
+                          </object>
+                        </child>
+
+                      </object>
+                    </child>
+
+                  </object>
+                </child>
+
+              </object>
+            </child>
+
+          </object>
+        </child>
+
+
+        <!-- Meetings -->
+        <child>
+          <object class="GtkSeparator">
+            <property name="visible" bind-source="meetings_listbox" bind-property="visible" 
bind-flags="default|sync-create" />
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkListBox" id="meetings_listbox">
+            <property name="selection-mode">none</property>
+            <style>
+              <class name="background" />
+            </style>
+          </object>
+        </child>
+
+
+        <!-- Edit button -->
+        <child>
+          <object class="GtkButton" id="edit_button">
+            <property name="visible">True</property>
+            <property name="can-default">True</property>
+            <property name="label" translatable="yes">Edit…</property>
+            <signal name="clicked" handler="on_edit_button_clicked_cb" object="GcalEventPopover" 
swapped="no" />
+          </object>
+        </child>
+
+      </object>
+    </child>
+
+  </template>
+</interface>
diff --git a/src/gui/gcal-event-widget.c b/src/gui/gcal-event-widget.c
index 99c9286f..dd34f1c4 100644
--- a/src/gui/gcal-event-widget.c
+++ b/src/gui/gcal-event-widget.c
@@ -25,6 +25,8 @@
 #include "gcal-application.h"
 #include "gcal-context.h"
 #include "gcal-clock.h"
+#include "gcal-debug.h"
+#include "gcal-event-popover.h"
 #include "gcal-event-widget.h"
 #include "gcal-utils.h"
 
@@ -32,6 +34,13 @@
 #define INTENSITY(c)  ((c->red) * 0.30 + (c->green) * 0.59 + (c->blue) * 0.11)
 #define ICON_SIZE     16
 
+typedef struct
+{
+  GcalEventWidget *event_widget;
+  GcalEventPreviewCallback callback;
+  gpointer user_data;
+} PreviewData;
+
 struct _GcalEventWidget
 {
   GtkBin              parent;
@@ -527,6 +536,40 @@ gcal_event_widget_set_event_internal (GcalEventWidget *self,
                           G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
 }
 
+static void
+reply_preview_callback (GtkWidget              *event_popover,
+                        PreviewData            *data,
+                        GcalEventPreviewAction  action)
+{
+  if (data->callback)
+    data->callback (data->event_widget, action, data->user_data);
+
+  g_signal_handlers_disconnect_by_data (event_popover, data);
+
+  gtk_widget_destroy (event_popover);
+
+  g_clear_pointer (&data, g_free);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_event_popover_closed_cb (GtkWidget   *event_popover,
+                            PreviewData *data)
+{
+  reply_preview_callback (event_popover, data, GCAL_EVENT_PREVIEW_ACTION_NONE);
+}
+
+static void
+on_event_popover_edit_cb (GtkWidget   *event_popover,
+                               PreviewData *data)
+{
+  reply_preview_callback (event_popover, data, GCAL_EVENT_PREVIEW_ACTION_EDIT);
+}
+
 
 /*
  * GtkWidget overrides
@@ -1134,6 +1177,40 @@ gcal_event_widget_set_date_start (GcalEventWidget *self,
     }
 }
 
+/**
+ * gcal_event_widget_show_preview:
+ * @self: a #GcalEventWidget
+ * @callback: (nullable): callback for the event preview
+ * @user_data: (nullable): user data for @callback
+ *
+ * Shows an event preview popover for this event widget, and
+ * calls @callback when the popover is either dismissed, or
+ * the edit button is clicked.
+ */
+void
+gcal_event_widget_show_preview (GcalEventWidget          *self,
+                                GcalEventPreviewCallback  callback,
+                                gpointer                  user_data)
+{
+  PreviewData *data;
+  GtkWidget *event_popover;
+
+  GCAL_ENTRY;
+
+  data = g_new0 (PreviewData, 1);
+  data->event_widget = self;
+  data->callback = callback;
+  data->user_data = user_data;
+
+  event_popover = gcal_event_popover_new (self->context, self->event);
+  gtk_popover_set_relative_to (GTK_POPOVER (event_popover), GTK_WIDGET (self));
+  g_signal_connect (event_popover, "closed", G_CALLBACK (on_event_popover_closed_cb), data);
+  g_signal_connect (event_popover, "edit", G_CALLBACK (on_event_popover_edit_cb), data);
+  gtk_popover_popup (GTK_POPOVER (event_popover));
+
+  GCAL_EXIT;
+}
+
 /**
  * gcal_event_widget_get_event:
  * @self: a #GcalEventWidget
diff --git a/src/gui/gcal-event-widget.h b/src/gui/gcal-event-widget.h
index 05ad16bc..c1bb81a1 100644
--- a/src/gui/gcal-event-widget.h
+++ b/src/gui/gcal-event-widget.h
@@ -26,10 +26,19 @@
 
 G_BEGIN_DECLS
 
-#define GCAL_TYPE_EVENT_WIDGET                    (gcal_event_widget_get_type ())
+typedef enum
+{
+  GCAL_EVENT_PREVIEW_ACTION_NONE,
+  GCAL_EVENT_PREVIEW_ACTION_EDIT,
+} GcalEventPreviewAction;
 
+#define GCAL_TYPE_EVENT_WIDGET                    (gcal_event_widget_get_type ())
 G_DECLARE_FINAL_TYPE (GcalEventWidget, gcal_event_widget, GCAL, EVENT_WIDGET, GtkBin)
 
+typedef void         (*GcalEventPreviewCallback)                 (GcalEventWidget        *event_widget,
+                                                                  GcalEventPreviewAction  action,
+                                                                  gpointer                user_data);
+
 GtkWidget*           gcal_event_widget_new                       (GcalContext        *context,
                                                                   GcalEvent          *event);
 
@@ -48,6 +57,10 @@ void                 gcal_event_widget_set_date_end              (GcalEventWidge
 void                 gcal_event_widget_set_read_only             (GcalEventWidget    *event,
                                                                   gboolean            read_only);
 
+void                 gcal_event_widget_show_preview              (GcalEventWidget          *self,
+                                                                  GcalEventPreviewCallback  callback,
+                                                                  gpointer                  user_data);
+
 /* Utilities */
 
 GtkWidget*           gcal_event_widget_clone                     (GcalEventWidget    *widget);
diff --git a/src/gui/gcal-meeting-row.c b/src/gui/gcal-meeting-row.c
new file mode 100644
index 00000000..ac2a0c54
--- /dev/null
+++ b/src/gui/gcal-meeting-row.c
@@ -0,0 +1,208 @@
+/* gcal-meeting-row.c
+ *
+ * Copyright 2021 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "GcalMeetingRow"
+
+#include "gcal-meeting-row.h"
+#include "gcal-utils.h"
+
+#include <glib/gi18n.h>
+
+struct _GcalMeetingRow
+{
+  GtkListBoxRow       parent_instance;
+
+  GtkLabel           *service_label;
+  GtkLabel           *url_label;
+
+  gchar              *url;
+};
+
+G_DEFINE_TYPE (GcalMeetingRow, gcal_meeting_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum
+{
+  PROP_0,
+  PROP_URL,
+  N_PROPS
+};
+
+enum
+{
+  JOIN_MEETING,
+  N_SIGNALS,
+};
+
+static guint signals[N_SIGNALS] = { 0, };
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+
+/*
+ * Auxiliary methods
+ */
+
+const gchar*
+get_service_name_from_url (const gchar *url)
+{
+  struct {
+    const gchar *needle;
+    const gchar *service_name;
+  } service_name_vtable[] = {
+    { "meet.google.com", N_("Google Meet") },
+    { "meet.jit.si", N_("Jitsi") },
+    { "whereby.com", N_("Whereby") },
+    { "zoom.us", N_("Zoom") },
+  };
+  gsize i;
+
+  for (i = 0; i < G_N_ELEMENTS (service_name_vtable); i++)
+    {
+      if (strstr (url, service_name_vtable[i].needle))
+        return gettext (service_name_vtable[i].service_name);
+    }
+
+  return _("Unknown Service");
+}
+
+static void
+setup_meeting (GcalMeetingRow *self)
+{
+  g_autofree gchar *markup_url = NULL;
+
+  gtk_label_set_label (self->service_label, get_service_name_from_url (self->url));
+
+  markup_url = g_strdup_printf ("<a href=\"%s\">%s</a>", self->url, self->url);
+  gtk_label_set_markup (self->url_label, markup_url);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_join_button_clicked_cb (GtkButton      *button,
+                           GcalMeetingRow *self)
+{
+  g_signal_emit (self, signals[JOIN_MEETING], 0, self->url);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gcal_meeting_row_finalize (GObject *object)
+{
+  GcalMeetingRow *self = (GcalMeetingRow *)object;
+
+  g_clear_pointer (&self->url, g_free);
+
+  G_OBJECT_CLASS (gcal_meeting_row_parent_class)->finalize (object);
+}
+
+static void
+gcal_meeting_row_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  GcalMeetingRow *self = GCAL_MEETING_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_URL:
+      g_value_set_string (value, self->url);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_meeting_row_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  GcalMeetingRow *self = GCAL_MEETING_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_URL:
+      g_assert (self->url == NULL);
+      self->url = g_value_dup_string (value);
+      setup_meeting (self);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_meeting_row_class_init (GcalMeetingRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gcal_meeting_row_finalize;
+  object_class->get_property = gcal_meeting_row_get_property;
+  object_class->set_property = gcal_meeting_row_set_property;
+
+  signals[JOIN_MEETING] = g_signal_new ("join-meeting",
+                                        GCAL_TYPE_MEETING_ROW,
+                                        G_SIGNAL_RUN_LAST,
+                                        0, NULL, NULL, NULL,
+                                        G_TYPE_NONE,
+                                        1,
+                                        G_TYPE_STRING);
+
+  properties[PROP_URL] = g_param_spec_string ("url",
+                                              "URL of the meeting",
+                                              "URL of the meeting",
+                                              NULL,
+                                              G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/calendar/ui/gui/gcal-meeting-row.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, GcalMeetingRow, service_label);
+  gtk_widget_class_bind_template_child (widget_class, GcalMeetingRow, url_label);
+
+  gtk_widget_class_bind_template_callback (widget_class, on_join_button_clicked_cb);
+}
+
+static void
+gcal_meeting_row_init (GcalMeetingRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget*
+gcal_meeting_row_new (const gchar *url)
+{
+  return g_object_new (GCAL_TYPE_MEETING_ROW,
+                       "url", url,
+                       NULL);
+}
diff --git a/src/gui/gcal-meeting-row.h b/src/gui/gcal-meeting-row.h
new file mode 100644
index 00000000..51367021
--- /dev/null
+++ b/src/gui/gcal-meeting-row.h
@@ -0,0 +1,33 @@
+/* gcal-meeting-row.h
+ *
+ * Copyright 2021 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_MEETING_ROW (gcal_meeting_row_get_type())
+
+G_DECLARE_FINAL_TYPE (GcalMeetingRow, gcal_meeting_row, GCAL, MEETING_ROW, GtkListBoxRow)
+
+GtkWidget*           gcal_meeting_row_new                        (const gchar        *url);
+
+G_END_DECLS
diff --git a/src/gui/gcal-meeting-row.ui b/src/gui/gcal-meeting-row.ui
new file mode 100644
index 00000000..71d4b96f
--- /dev/null
+++ b/src/gui/gcal-meeting-row.ui
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.14"/>
+  <template class="GcalMeetingRow" parent="GtkListBoxRow">
+    <property name="visible">True</property>
+    <property name="activatable">False</property>
+    <focus-chain>
+      <widget name="url_label"/>
+      <widget name="join_button"/>
+    </focus-chain>
+
+    <child>
+      <object class="GtkGrid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin-start">12</property>
+        <property name="margin-end">12</property>
+        <property name="margin-top">6</property>
+        <property name="margin-bottom">6</property>
+        <property name="column-spacing">18</property>
+
+        <child>
+          <object class="GtkLabel" id="service_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="hexpand">True</property>
+            <property name="xalign">0.0</property>
+            <style>
+              <class name="title" />
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">0</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkLabel" id="url_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="hexpand">True</property>
+            <property name="xalign">0.0</property>
+            <style>
+              <class name="subtitle" />
+              <class name="dim-label" />
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">1</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkButton" id="join_button">
+            <property name="visible">True</property>
+            <property name="can-focus">True</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <!-- Translators: "Join" as in "Join meeting" -->
+            <property name="label" translatable="yes">Join</property>
+            <style>
+              <class name="suggested-action" />
+            </style>
+            <signal name="clicked" handler="on_join_button_clicked_cb" object="GcalMeetingRow" swapped="no" 
/>
+          </object>
+          <packing>
+            <property name="top-attach">0</property>
+            <property name="left-attach">1</property>
+            <property name="height">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+
+  </template>
+</interface>
diff --git a/src/gui/gcal-window.c b/src/gui/gcal-window.c
index dd1ecadc..65183985 100644
--- a/src/gui/gcal-window.c
+++ b/src/gui/gcal-window.c
@@ -636,17 +636,34 @@ create_event_detailed_cb (GcalView *view,
   g_clear_object (&comp);
 }
 
+static void
+event_preview_cb (GcalEventWidget        *event_widget,
+                  GcalEventPreviewAction  action,
+                  gpointer                user_data)
+{
+  GcalWindow *self = GCAL_WINDOW (user_data);
+  GcalEvent *event;
+
+  switch (action)
+    {
+    case GCAL_EVENT_PREVIEW_ACTION_EDIT:
+      event = gcal_event_widget_get_event (event_widget);
+      gcal_event_editor_dialog_set_event (self->event_editor, event, FALSE);
+      gtk_window_present (GTK_WINDOW (self->event_editor));
+      break;
+
+    case GCAL_EVENT_PREVIEW_ACTION_NONE:
+    default:
+      break;
+    }
+}
+
 static void
 event_activated (GcalView        *view,
                  GcalEventWidget *event_widget,
                  gpointer         user_data)
 {
-  GcalWindow *window = GCAL_WINDOW (user_data);
-  GcalEvent *event;
-
-  event = gcal_event_widget_get_event (event_widget);
-  gcal_event_editor_dialog_set_event (window->event_editor, event, FALSE);
-  gtk_widget_show (GTK_WIDGET (window->event_editor));
+  gcal_event_widget_show_preview (event_widget, event_preview_cb, user_data);
 }
 
 static void
diff --git a/src/gui/gui.gresource.xml b/src/gui/gui.gresource.xml
index 5c481b5b..1ecfe3ca 100644
--- a/src/gui/gui.gresource.xml
+++ b/src/gui/gui.gresource.xml
@@ -2,7 +2,9 @@
 <gresources>
   <gresource prefix="/org/gnome/calendar/ui/gui">
     <file compressed="true">gcal-calendar-popover.ui</file>
+    <file compressed="true">gcal-event-popover.ui</file>
     <file compressed="true">gcal-event-widget.ui</file>
+    <file compressed="true">gcal-meeting-row.ui</file>
     <file compressed="true">gcal-quick-add-popover.ui</file>
     <file compressed="true">gcal-weather-settings.ui</file>
     <file compressed="true">gcal-window.ui</file>
diff --git a/src/gui/meson.build b/src/gui/meson.build
index d4f55962..6a076076 100644
--- a/src/gui/meson.build
+++ b/src/gui/meson.build
@@ -15,8 +15,10 @@ built_sources += gnome.compile_resources(
 sources += files(
   'gcal-application.c',
   'gcal-calendar-popover.c',
+  'gcal-event-popover.c',
   'gcal-event-widget.c',
   'gcal-expandable-entry.c',
+  'gcal-meeting-row.c',
   'gcal-quick-add-popover.c',
   'gcal-search-button.c',
   'gcal-weather-settings.c',
diff --git a/src/theme/Adwaita.css b/src/theme/Adwaita.css
index c674a3fa..7658eb4d 100644
--- a/src/theme/Adwaita.css
+++ b/src/theme/Adwaita.css
@@ -225,6 +225,25 @@ event.color-light:backdrop {
     color: rgba(0, 0, 0, 0.3);
 }
 
+/* Event Popover */
+popover.event-popover,
+popover.event-popover > box {
+  padding: 0;
+}
+
+popover.event-popover > box > button {
+  border-left-width: 0;
+  border-right-width: 0;
+  border-bottom-width: 0;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+popover.event-popover textview.view,
+popover.event-popover textview.view text {
+  background-color: transparent;
+}
+
 .search-viewport {
   background-color: @theme_base_color;
 }


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