[gnome-software/wip/hughsie/GsPluginEvent: 24/24] Allow plugins to report non-critical events up to the UI



commit d5bf5bb7b0b9784c6ffd4146888835827f22d39a
Author: Richard Hughes <richard hughsie com>
Date:   Tue Sep 6 18:49:12 2016 +0100

    Allow plugins to report non-critical events up to the UI
    
    This also removes the hacky last_error GsApp private API and thus converts the
    ugly modal failure dialogs to in-app notifications.
    
    To do this, automatically create GsPluginEvent objects for certain errors.
    
    Fixes: https://bugzilla.gnome.org/show_bug.cgi?id=770918

 src/Makefile.am               |    6 +
 src/gd-notification.c         |  875 +++++++++++++++++++++++++++++++++++++++++
 src/gd-notification.h         |   67 ++++
 src/gnome-software.ui         |  236 ++++++++---
 src/gs-app-private.h          |    3 -
 src/gs-app.c                  |   40 --
 src/gs-application.c          |    5 +
 src/gs-page.c                 |   28 --
 src/gs-plugin-event.c         |  304 ++++++++++++++
 src/gs-plugin-event.h         |   83 ++++
 src/gs-plugin-loader.c        |  235 +++++++++++-
 src/gs-plugin-loader.h        |    7 +
 src/gs-plugin-vfuncs.h        |    9 +-
 src/gs-plugin.c               |   42 ++
 src/gs-plugin.h               |    6 +-
 src/gs-self-test.c            |   31 ++-
 src/gs-shell-updates.c        |   13 -
 src/gs-shell.c                |  208 ++++++++++
 src/plugins/gs-plugin-dummy.c |    5 +-
 19 files changed, 2036 insertions(+), 167 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index b8677dd..12055b5 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -115,6 +115,7 @@ gnome_software_cmd_SOURCES =                                \
        gs-debug.c                                      \
        gs-utils.c                                      \
        gs-os-release.c                                 \
+       gs-plugin-event.c                               \
        gs-plugin-loader.c                              \
        gs-plugin-loader-sync.c                         \
        gs-category.c                                   \
@@ -137,6 +138,8 @@ bin_PROGRAMS =                                              \
 
 gnome_software_SOURCES =                               \
        gnome-software.h                                \
+       gd-notification.c                               \
+       gd-notification.h                               \
        gs-utils.c                                      \
        gs-utils.h                                      \
        gs-app.c                                        \
@@ -192,6 +195,8 @@ gnome_software_SOURCES =                            \
        gs-page.h                                       \
        gs-plugin.c                                     \
        gs-plugin.h                                     \
+       gs-plugin-event.c                               \
+       gs-plugin-event.h                               \
        gs-plugin-private.h                             \
        gs-plugin-vfuncs.h                              \
        gs-progress-button.c                            \
@@ -340,6 +345,7 @@ gs_self_test_SOURCES =                                              \
        gs-category.c                                           \
        gs-common.c                                             \
        gs-os-release.c                                         \
+       gs-plugin-event.c                                       \
        gs-plugin-loader-sync.c                                 \
        gs-plugin-loader.c                                      \
        gs-plugin.c                                             \
diff --git a/src/gd-notification.c b/src/gd-notification.c
new file mode 100644
index 0000000..8153436
--- /dev/null
+++ b/src/gd-notification.c
@@ -0,0 +1,875 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * gd-notification
+ * Based on gtk-notification from gnome-contacts:
+ * http://git.gnome.org/browse/gnome-contacts/tree/src/gtk-notification.c?id=3.3.91
+ *
+ * Copyright (C) Erick Pérez Castellanos 2011 <erick red gmail com>
+ * Copyright (C) 2012 Red Hat, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 2 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.";
+ */
+
+#include "gd-notification.h"
+
+/**
+ * SECTION:gdnotification
+ * @short_description: Report notification messages to the user
+ * @include: gtk/gtk.h
+ * @see_also: #GtkStatusbar, #GtkMessageDialog, #GtkInfoBar
+ *
+ * #GdNotification is a widget made for showing notifications to
+ * the user, allowing them to close the notification or wait for it
+ * to time out.
+ *
+ * #GdNotification provides one signal (#GdNotification::dismissed), for when the notification
+ * times out or is closed.
+ *
+ */
+
+#define GTK_PARAM_READWRITE G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB
+#define SHADOW_OFFSET_X 2
+#define SHADOW_OFFSET_Y 3
+#define ANIMATION_TIME 200 /* msec */
+#define ANIMATION_STEP 40 /* msec */
+
+enum {
+  PROP_0,
+  PROP_TIMEOUT,
+  PROP_SHOW_CLOSE_BUTTON
+};
+
+struct _GdNotificationPrivate {
+  GtkWidget *close_button;
+  gboolean show_close_button;
+
+  GdkWindow *bin_window;
+
+  int animate_y; /* from 0 to allocation.height */
+  gboolean waiting_for_viewable;
+  gboolean revealed;
+  gboolean dismissed;
+  gboolean sent_dismissed;
+  guint animate_timeout;
+
+  gint timeout;
+  guint timeout_source_id;
+};
+
+enum {
+  DISMISSED,
+  LAST_SIGNAL
+};
+
+static guint notification_signals[LAST_SIGNAL] = { 0 };
+
+static gboolean gd_notification_draw                           (GtkWidget       *widget,
+                                                                 cairo_t         *cr);
+static void     gd_notification_get_preferred_width            (GtkWidget       *widget,
+                                                                 gint            *minimum_size,
+                                                                 gint            *natural_size);
+static void     gd_notification_get_preferred_height_for_width (GtkWidget       *widget,
+                                                                 gint             width,
+                                                                 gint            *minimum_height,
+                                                                 gint            *natural_height);
+static void     gd_notification_get_preferred_height           (GtkWidget       *widget,
+                                                                 gint            *minimum_size,
+                                                                 gint            *natural_size);
+static void     gd_notification_get_preferred_width_for_height (GtkWidget       *widget,
+                                                                 gint             height,
+                                                                 gint            *minimum_width,
+                                                                 gint            *natural_width);
+static void     gd_notification_size_allocate                  (GtkWidget       *widget,
+                                                                 GtkAllocation   *allocation);
+static gboolean gd_notification_timeout_cb                     (gpointer         user_data);
+static void     gd_notification_show                           (GtkWidget       *widget);
+static void     gd_notification_add                            (GtkContainer    *container,
+                                                                 GtkWidget       *child);
+
+/* signals handlers */
+static void     gd_notification_close_button_clicked_cb        (GtkWidget       *widget,
+                                                                 gpointer         user_data);
+
+G_DEFINE_TYPE(GdNotification, gd_notification, GTK_TYPE_BIN);
+
+static void
+gd_notification_init (GdNotification *notification)
+{
+  GtkWidget *close_button_image;
+  GtkStyleContext *context;
+  GdNotificationPrivate *priv;
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (notification));
+  gtk_style_context_add_class (context, GTK_STYLE_CLASS_FRAME);
+  gtk_style_context_add_class (context, "app-notification");
+
+  gtk_widget_set_halign (GTK_WIDGET (notification), GTK_ALIGN_CENTER);
+  gtk_widget_set_valign (GTK_WIDGET (notification), GTK_ALIGN_START);
+
+  gtk_widget_set_has_window (GTK_WIDGET (notification), TRUE);
+
+  priv = notification->priv =
+    G_TYPE_INSTANCE_GET_PRIVATE (notification,
+                                 GD_TYPE_NOTIFICATION,
+                                 GdNotificationPrivate);
+
+  priv->animate_y = 0;
+  priv->close_button = gtk_button_new ();
+  gtk_widget_set_parent (priv->close_button, GTK_WIDGET (notification));
+  gtk_widget_show (priv->close_button);
+  g_object_set (priv->close_button,
+                "relief", GTK_RELIEF_NONE,
+                "focus-on-click", FALSE,
+                NULL);
+  g_signal_connect (priv->close_button,
+                    "clicked",
+                    G_CALLBACK (gd_notification_close_button_clicked_cb),
+                    notification);
+  close_button_image = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_BUTTON);
+  gtk_button_set_image (GTK_BUTTON (notification->priv->close_button), close_button_image);
+
+  priv->timeout_source_id = 0;
+}
+
+static void
+gd_notification_finalize (GObject *object)
+{
+  GdNotification *notification;
+  GdNotificationPrivate *priv;
+
+  g_return_if_fail (GTK_IS_NOTIFICATION (object));
+
+  notification = GD_NOTIFICATION (object);
+  priv = notification->priv;
+
+  if (priv->animate_timeout != 0)
+    g_source_remove (priv->animate_timeout);
+
+  if (priv->timeout_source_id != 0)
+    g_source_remove (priv->timeout_source_id);
+
+  G_OBJECT_CLASS (gd_notification_parent_class)->finalize (object);
+}
+
+static void
+gd_notification_destroy (GtkWidget *widget)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+
+  if (!priv->sent_dismissed)
+    {
+      g_signal_emit (notification, notification_signals[DISMISSED], 0);
+      priv->sent_dismissed = TRUE;
+    }
+
+  if (priv->close_button)
+    {
+      gtk_widget_unparent (priv->close_button);
+      priv->close_button = NULL;
+    }
+
+  GTK_WIDGET_CLASS (gd_notification_parent_class)->destroy (widget);
+}
+
+static void
+gd_notification_realize (GtkWidget *widget)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+  GtkBin *bin = GTK_BIN (widget);
+  GtkAllocation allocation;
+  GtkWidget *child;
+  GdkWindow *window;
+  GdkWindowAttr attributes;
+  gint attributes_mask;
+
+  gtk_widget_set_realized (widget, TRUE);
+
+  gtk_widget_get_allocation (widget, &allocation);
+
+  attributes.x = allocation.x;
+  attributes.y = allocation.y;
+  attributes.width = allocation.width;
+  attributes.height = allocation.height;
+  attributes.window_type = GDK_WINDOW_CHILD;
+  attributes.wclass = GDK_INPUT_OUTPUT;
+  attributes.visual = gtk_widget_get_visual (widget);
+
+  attributes.event_mask = GDK_VISIBILITY_NOTIFY_MASK | GDK_EXPOSURE_MASK;
+
+  attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+  window = gdk_window_new (gtk_widget_get_parent_window (widget),
+                           &attributes, attributes_mask);
+  gtk_widget_set_window (widget, window);
+  gtk_widget_register_window (widget, window);
+
+  attributes.x = 0;
+  attributes.y = attributes.height + priv->animate_y;
+  attributes.event_mask = gtk_widget_get_events (widget) |
+                          GDK_EXPOSURE_MASK |
+                          GDK_VISIBILITY_NOTIFY_MASK |
+                          GDK_ENTER_NOTIFY_MASK |
+                          GDK_LEAVE_NOTIFY_MASK;
+
+  priv->bin_window = gdk_window_new (window, &attributes, attributes_mask);
+  gtk_widget_register_window (widget, priv->bin_window);
+
+  child = gtk_bin_get_child (bin);
+  if (child)
+    gtk_widget_set_parent_window (child, priv->bin_window);
+  gtk_widget_set_parent_window (priv->close_button, priv->bin_window);
+
+  gdk_window_show (priv->bin_window);
+}
+
+static void
+gd_notification_unrealize (GtkWidget *widget)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+
+  gtk_widget_unregister_window (widget, priv->bin_window);
+  gdk_window_destroy (priv->bin_window);
+  priv->bin_window = NULL;
+
+  GTK_WIDGET_CLASS (gd_notification_parent_class)->unrealize (widget);
+}
+
+static int
+animation_target (GdNotification *notification)
+{
+  GdNotificationPrivate *priv = notification->priv;
+  GtkAllocation allocation;
+
+  if (priv->revealed) {
+    gtk_widget_get_allocation (GTK_WIDGET (notification), &allocation);
+    return allocation.height;
+  } else {
+    return 0;
+  }
+}
+
+static gboolean
+animation_timeout_cb (gpointer user_data)
+{
+  GdNotification *notification = GD_NOTIFICATION (user_data);
+  GdNotificationPrivate *priv = notification->priv;
+  GtkAllocation allocation;
+  int target, delta;
+
+  target = animation_target (notification);
+
+  if (priv->animate_y != target) {
+    gtk_widget_get_allocation (GTK_WIDGET (notification), &allocation);
+
+    delta = allocation.height * ANIMATION_STEP / ANIMATION_TIME;
+
+    if (priv->revealed)
+      priv->animate_y += delta;
+    else
+      priv->animate_y -= delta;
+
+    priv->animate_y = CLAMP (priv->animate_y, 0, allocation.height);
+
+    if (priv->bin_window != NULL)
+      gdk_window_move (priv->bin_window,
+                       0,
+                       -allocation.height + priv->animate_y);
+    return G_SOURCE_CONTINUE;
+  }
+
+  if (priv->dismissed && priv->animate_y == 0)
+    gtk_widget_destroy (GTK_WIDGET (notification));
+
+  priv->animate_timeout = 0;
+  return G_SOURCE_REMOVE;
+}
+
+static void
+start_animation (GdNotification *notification)
+{
+  GdNotificationPrivate *priv = notification->priv;
+  int target;
+
+  if (priv->animate_timeout != 0)
+    return; /* Already running */
+
+  target = animation_target (notification);
+  if (priv->animate_y != target)
+    notification->priv->animate_timeout =
+      gdk_threads_add_timeout (ANIMATION_STEP,
+                               animation_timeout_cb,
+                               notification);
+}
+
+static void
+gd_notification_show (GtkWidget *widget)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+
+  GTK_WIDGET_CLASS (gd_notification_parent_class)->show (widget);
+  priv->revealed = TRUE;
+  priv->waiting_for_viewable = TRUE;
+}
+
+static void
+gd_notification_hide (GtkWidget *widget)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+
+  GTK_WIDGET_CLASS (gd_notification_parent_class)->hide (widget);
+  priv->revealed = FALSE;
+  priv->waiting_for_viewable = FALSE;
+}
+
+static void
+gd_notification_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+  GdNotification *notification = GD_NOTIFICATION (object);
+
+  g_return_if_fail (GTK_IS_NOTIFICATION (object));
+
+  switch (prop_id) {
+  case PROP_TIMEOUT:
+    gd_notification_set_timeout (notification,
+                                 g_value_get_int (value));
+    break;
+  case PROP_SHOW_CLOSE_BUTTON:
+    gd_notification_set_show_close_button (notification,
+                                           g_value_get_boolean (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    break;
+  }
+}
+
+static void
+gd_notification_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+  g_return_if_fail (GTK_IS_NOTIFICATION (object));
+  GdNotification *notification = GD_NOTIFICATION (object);
+
+  switch (prop_id) {
+  case PROP_TIMEOUT:
+    g_value_set_int (value, notification->priv->timeout);
+    break;
+  case PROP_SHOW_CLOSE_BUTTON:
+    g_value_set_boolean (value,
+                         notification->priv->show_close_button);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    break;
+  }
+}
+
+static void
+gd_notification_forall (GtkContainer *container,
+                         gboolean      include_internals,
+                         GtkCallback   callback,
+                         gpointer      callback_data)
+{
+  GtkBin *bin = GTK_BIN (container);
+  GdNotification *notification = GD_NOTIFICATION (container);
+  GdNotificationPrivate *priv = notification->priv;
+  GtkWidget *child;
+
+  child = gtk_bin_get_child (bin);
+  if (child)
+    (* callback) (child, callback_data);
+
+  if (include_internals)
+    (* callback) (priv->close_button, callback_data);
+}
+
+static void
+unqueue_autohide (GdNotification *notification)
+{
+  GdNotificationPrivate *priv = notification->priv;
+
+  if (priv->timeout_source_id)
+    {
+      g_source_remove (priv->timeout_source_id);
+      priv->timeout_source_id = 0;
+    }
+}
+
+static void
+queue_autohide (GdNotification *notification)
+{
+  GdNotificationPrivate *priv = notification->priv;
+
+  if (priv->timeout_source_id == 0 &&
+      priv->timeout != -1)
+    priv->timeout_source_id =
+      gdk_threads_add_timeout (priv->timeout * 1000,
+                               gd_notification_timeout_cb,
+                               notification);
+}
+
+static gboolean
+gd_notification_visibility_notify_event (GtkWidget          *widget,
+                                          GdkEventVisibility  *event)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+
+  if (!gtk_widget_get_visible (widget))
+    return FALSE;
+
+  if (priv->waiting_for_viewable)
+    {
+      start_animation (notification);
+      priv->waiting_for_viewable = FALSE;
+    }
+
+  queue_autohide (notification);
+
+  return FALSE;
+}
+
+static gboolean
+gd_notification_enter_notify (GtkWidget        *widget,
+                              GdkEventCrossing *event)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+
+  if ((event->window == priv->bin_window) &&
+      (event->detail != GDK_NOTIFY_INFERIOR))
+    {
+      unqueue_autohide (notification);
+    }
+
+  return FALSE;
+}
+
+static gboolean
+gd_notification_leave_notify (GtkWidget        *widget,
+                              GdkEventCrossing *event)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+
+  if ((event->window == priv->bin_window) &&
+      (event->detail != GDK_NOTIFY_INFERIOR))
+    {
+      queue_autohide (notification);
+    }
+
+  return FALSE;
+}
+
+static void
+gd_notification_class_init (GdNotificationClass *klass)
+{
+  GObjectClass* object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->finalize = gd_notification_finalize;
+  object_class->set_property = gd_notification_set_property;
+  object_class->get_property = gd_notification_get_property;
+
+  widget_class->show = gd_notification_show;
+  widget_class->hide = gd_notification_hide;
+  widget_class->destroy = gd_notification_destroy;
+  widget_class->get_preferred_width = gd_notification_get_preferred_width;
+  widget_class->get_preferred_height_for_width = gd_notification_get_preferred_height_for_width;
+  widget_class->get_preferred_height = gd_notification_get_preferred_height;
+  widget_class->get_preferred_width_for_height = gd_notification_get_preferred_width_for_height;
+  widget_class->size_allocate = gd_notification_size_allocate;
+  widget_class->draw = gd_notification_draw;
+  widget_class->realize = gd_notification_realize;
+  widget_class->unrealize = gd_notification_unrealize;
+  widget_class->visibility_notify_event = gd_notification_visibility_notify_event;
+  widget_class->enter_notify_event = gd_notification_enter_notify;
+  widget_class->leave_notify_event = gd_notification_leave_notify;
+
+  container_class->add = gd_notification_add;
+  container_class->forall = gd_notification_forall;
+  gtk_container_class_handle_border_width (container_class);
+
+
+  /**
+   * GdNotification:timeout:
+   *
+   * The time it takes to hide the widget, in seconds.
+   *
+   * Since: 0.1
+   */
+  g_object_class_install_property (object_class,
+                                   PROP_TIMEOUT,
+                                   g_param_spec_int("timeout", "timeout",
+                                                    "The time it takes to hide the widget, in seconds",
+                                                    -1, G_MAXINT, -1,
+                                                    GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+  g_object_class_install_property (object_class,
+                                   PROP_SHOW_CLOSE_BUTTON,
+                                   g_param_spec_boolean("show-close-button", "show-close-button",
+                                                        "Whether to show a stock close button that dismisses 
the notification",
+                                                        TRUE,
+                                                        GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+  notification_signals[DISMISSED] = g_signal_new ("dismissed",
+                                                  G_OBJECT_CLASS_TYPE (klass),
+                                                  G_SIGNAL_RUN_LAST,
+                                                  G_STRUCT_OFFSET (GdNotificationClass, dismissed),
+                                                  NULL,
+                                                  NULL,
+                                                  g_cclosure_marshal_VOID__VOID,
+                                                  G_TYPE_NONE,
+                                                  0);
+
+  g_type_class_add_private (object_class, sizeof (GdNotificationPrivate));
+}
+
+static void
+get_padding_and_border (GdNotification *notification,
+                        GtkBorder *border)
+{
+  GtkStyleContext *context;
+  GtkStateFlags state;
+  GtkBorder tmp;
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (notification));
+  state = gtk_widget_get_state_flags (GTK_WIDGET (notification));
+
+  gtk_style_context_get_padding (context, state, border);
+
+  gtk_style_context_get_border (context, state, &tmp);
+  border->top += tmp.top;
+  border->right += tmp.right;
+  border->bottom += tmp.bottom;
+  border->left += tmp.left;
+}
+
+static gboolean
+gd_notification_draw (GtkWidget *widget, cairo_t *cr)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+  GtkStyleContext *context;
+
+  if (gtk_cairo_should_draw_window (cr, priv->bin_window))
+    {
+      context = gtk_widget_get_style_context (widget);
+
+      gtk_render_background (context,  cr,
+                             0, 0,
+                             gtk_widget_get_allocated_width (widget),
+                             gtk_widget_get_allocated_height (widget));
+      gtk_render_frame (context,cr,
+                        0, 0,
+                        gtk_widget_get_allocated_width (widget),
+                        gtk_widget_get_allocated_height (widget));
+
+
+      if (GTK_WIDGET_CLASS (gd_notification_parent_class)->draw)
+        GTK_WIDGET_CLASS (gd_notification_parent_class)->draw(widget, cr);
+    }
+
+  return FALSE;
+}
+
+static void
+gd_notification_add (GtkContainer *container,
+                      GtkWidget    *child)
+{
+  GtkBin *bin = GTK_BIN (container);
+  GdNotification *notification = GD_NOTIFICATION (bin);
+  GdNotificationPrivate *priv = notification->priv;
+
+  g_return_if_fail (gtk_bin_get_child (bin) == NULL);
+
+  gtk_widget_set_parent_window (child, priv->bin_window);
+
+  GTK_CONTAINER_CLASS (gd_notification_parent_class)->add (container, child);
+}
+
+
+static void
+gd_notification_get_preferred_width (GtkWidget *widget, gint *minimum_size, gint *natural_size)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+  GtkBin *bin = GTK_BIN (widget);
+  gint child_min, child_nat;
+  GtkWidget *child;
+  GtkBorder padding;
+  gint minimum, natural;
+
+  get_padding_and_border (notification, &padding);
+
+  minimum = 0;
+  natural = 0;
+
+  child = gtk_bin_get_child (bin);
+  if (child && gtk_widget_get_visible (child))
+    {
+      gtk_widget_get_preferred_width (child,
+                                      &child_min, &child_nat);
+      minimum += child_min;
+      natural += child_nat;
+    }
+
+  if (priv->show_close_button)
+    {
+      gtk_widget_get_preferred_width (priv->close_button,
+                                      &child_min, &child_nat);
+      minimum += child_min;
+      natural += child_nat;
+    }
+
+  minimum += padding.left + padding.right + 2 * SHADOW_OFFSET_X;
+  natural += padding.left + padding.right + 2 * SHADOW_OFFSET_X;
+
+ if (minimum_size)
+    *minimum_size = minimum;
+
+  if (natural_size)
+    *natural_size = natural;
+}
+
+static void
+gd_notification_get_preferred_width_for_height (GtkWidget *widget,
+                                                 gint height,
+                                                 gint *minimum_width,
+                                                 gint *natural_width)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+  GtkBin *bin = GTK_BIN (widget);
+  gint child_min, child_nat, child_height;
+  GtkWidget *child;
+  GtkBorder padding;
+  gint minimum, natural;
+
+  get_padding_and_border (notification, &padding);
+
+  minimum = 0;
+  natural = 0;
+
+  child_height = height - SHADOW_OFFSET_Y - padding.top - padding.bottom;
+
+  child = gtk_bin_get_child (bin);
+  if (child && gtk_widget_get_visible (child))
+    {
+      gtk_widget_get_preferred_width_for_height (child, child_height,
+                                                 &child_min, &child_nat);
+      minimum += child_min;
+      natural += child_nat;
+    }
+
+  if (priv->show_close_button)
+    {
+      gtk_widget_get_preferred_width_for_height (priv->close_button, child_height,
+                                                 &child_min, &child_nat);
+      minimum += child_min;
+      natural += child_nat;
+    }
+
+  minimum += padding.left + padding.right + 2 * SHADOW_OFFSET_X;
+  natural += padding.left + padding.right + 2 * SHADOW_OFFSET_X;
+
+ if (minimum_width)
+    *minimum_width = minimum;
+
+  if (natural_width)
+    *natural_width = natural;
+}
+
+static void
+gd_notification_get_preferred_height_for_width (GtkWidget *widget,
+                                                 gint width,
+                                                 gint *minimum_height,
+                                                 gint *natural_height)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+  GtkBin *bin = GTK_BIN (widget);
+  gint child_min, child_nat, child_width, button_width = 0;
+  GtkWidget *child;
+  GtkBorder padding;
+  gint minimum = 0, natural = 0;
+
+  get_padding_and_border (notification, &padding);
+
+  if (priv->show_close_button)
+    {
+      gtk_widget_get_preferred_height (priv->close_button,
+                                       &minimum, &natural);
+      gtk_widget_get_preferred_width (priv->close_button,
+                                      NULL, &button_width);
+    }
+
+  child = gtk_bin_get_child (bin);
+  if (child && gtk_widget_get_visible (child))
+    {
+      child_width = width - button_width -
+        2 * SHADOW_OFFSET_X - padding.left - padding.right;
+
+      gtk_widget_get_preferred_height_for_width (child, child_width,
+                                                 &child_min, &child_nat);
+      minimum = MAX (minimum, child_min);
+      natural = MAX (natural, child_nat);
+    }
+
+  minimum += padding.top + padding.bottom + SHADOW_OFFSET_Y;
+  natural += padding.top + padding.bottom + SHADOW_OFFSET_Y;
+
+ if (minimum_height)
+    *minimum_height = minimum;
+
+  if (natural_height)
+    *natural_height = natural;
+}
+
+static void
+gd_notification_get_preferred_height (GtkWidget *widget, 
+                                      gint *minimum_height, 
+                                      gint *natural_height)
+{
+  gint width;
+
+  gd_notification_get_preferred_width (widget, &width, NULL);
+  gd_notification_get_preferred_height_for_width (widget, width,
+                                                  minimum_height, natural_height);
+}
+
+static void
+gd_notification_size_allocate (GtkWidget *widget,
+                                GtkAllocation *allocation)
+{
+  GdNotification *notification = GD_NOTIFICATION (widget);
+  GdNotificationPrivate *priv = notification->priv;
+  GtkBin *bin = GTK_BIN (widget);
+  GtkAllocation child_allocation;
+  GtkBorder padding;
+  GtkRequisition button_req;
+  GtkWidget *child;
+
+  gtk_widget_set_allocation (widget, allocation);
+
+  /* If somehow the notification changes while not hidden
+     and we're not animating, immediately follow the resize */
+  if (priv->animate_y > 0 &&
+      !priv->animate_timeout)
+    priv->animate_y = allocation->height;
+
+  get_padding_and_border (notification, &padding);
+
+  if (gtk_widget_get_realized (widget))
+    {
+      gdk_window_move_resize (gtk_widget_get_window (widget),
+                              allocation->x,
+                              allocation->y,
+                              allocation->width,
+                              allocation->height);
+      gdk_window_move_resize (priv->bin_window,
+                              0,
+                              -allocation->height + priv->animate_y,
+                              allocation->width,
+                              allocation->height);
+    }
+
+  child_allocation.x = SHADOW_OFFSET_X + padding.left;
+  child_allocation.y = padding.top;
+
+  if (priv->show_close_button)
+    gtk_widget_get_preferred_size (priv->close_button, &button_req, NULL);
+  else
+    button_req.width = button_req.height = 0;
+
+  child_allocation.height = MAX (1, allocation->height - SHADOW_OFFSET_Y - padding.top - padding.bottom);
+  child_allocation.width = MAX (1, (allocation->width - button_req.width -
+                                    2 * SHADOW_OFFSET_X - padding.left - padding.right));
+
+  child = gtk_bin_get_child (bin);
+  if (child && gtk_widget_get_visible (child))
+    gtk_widget_size_allocate (child, &child_allocation);
+
+  if (priv->show_close_button)
+    {
+      child_allocation.x += child_allocation.width;
+      child_allocation.width = button_req.width;
+      child_allocation.y += (child_allocation.height - button_req.height) / 2;
+      child_allocation.height = button_req.height;
+
+      gtk_widget_size_allocate (priv->close_button, &child_allocation);
+    }
+}
+
+static gboolean
+gd_notification_timeout_cb (gpointer user_data)
+{
+  GdNotification *notification = GD_NOTIFICATION (user_data);
+
+  gd_notification_dismiss (notification);
+
+  return G_SOURCE_REMOVE;
+}
+
+void
+gd_notification_set_timeout (GdNotification *notification,
+                             gint            timeout_sec)
+{
+  GdNotificationPrivate *priv = notification->priv;
+
+  priv->timeout = timeout_sec;
+  g_object_notify (G_OBJECT (notification), "timeout");
+}
+
+void
+gd_notification_set_show_close_button (GdNotification *notification,
+                                       gboolean show_close_button)
+{
+  GdNotificationPrivate *priv = notification->priv;
+
+  priv->show_close_button = show_close_button;
+
+  gtk_widget_set_visible (priv->close_button, show_close_button);
+  gtk_widget_queue_resize (GTK_WIDGET (notification));
+}
+
+void
+gd_notification_dismiss (GdNotification *notification)
+{
+  GdNotificationPrivate *priv = notification->priv;
+
+  unqueue_autohide (notification);
+
+  priv->dismissed = TRUE;
+  priv->revealed = FALSE;
+  start_animation (notification);
+}
+
+static void
+gd_notification_close_button_clicked_cb (GtkWidget *widget, gpointer user_data)
+{
+  GdNotification *notification = GD_NOTIFICATION(user_data);
+
+  gd_notification_dismiss (notification);
+}
+
+GtkWidget *
+gd_notification_new (void)
+{
+  return g_object_new (GD_TYPE_NOTIFICATION, NULL);
+}
diff --git a/src/gd-notification.h b/src/gd-notification.h
new file mode 100644
index 0000000..8efa191
--- /dev/null
+++ b/src/gd-notification.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * gd-notification
+ * Based on gtk-notification from gnome-contacts:
+ * http://git.gnome.org/browse/gnome-contacts/tree/src/gtk-notification.c?id=3.3.91
+ *
+ * Copyright (C) Erick Pérez Castellanos 2011 <erick red gmail com>
+ * Copyright (C) 2012 Red Hat, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 2 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.";
+ */
+
+#ifndef _GD_NOTIFICATION_H_
+#define _GD_NOTIFICATION_H_
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GD_TYPE_NOTIFICATION             (gd_notification_get_type ())
+#define GD_NOTIFICATION(obj)             (G_TYPE_CHECK_INSTANCE_CAST ((obj), GD_TYPE_NOTIFICATION, 
GdNotification))
+#define GD_NOTIFICATION_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST ((klass), GD_TYPE_NOTIFICATION, 
GdNotificationClass))
+#define GTK_IS_NOTIFICATION(obj)          (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GD_TYPE_NOTIFICATION))
+#define GTK_IS_NOTIFICATION_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE ((klass), GD_TYPE_NOTIFICATION))
+#define GD_NOTIFICATION_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS ((obj), GD_TYPE_NOTIFICATION, 
GdNotificationClass))
+
+typedef struct _GdNotificationPrivate GdNotificationPrivate;
+typedef struct _GdNotificationClass GdNotificationClass;
+typedef struct _GdNotification GdNotification;
+
+struct _GdNotificationClass {
+  GtkBinClass parent_class;
+
+  /* Signals */
+  void (*dismissed) (GdNotification *self);
+};
+
+struct _GdNotification {
+  GtkBin parent_instance;
+
+  /*< private > */
+  GdNotificationPrivate *priv;
+};
+
+GType gd_notification_get_type (void) G_GNUC_CONST;
+
+GtkWidget *gd_notification_new         (void);
+void       gd_notification_set_timeout (GdNotification *notification,
+                                        gint            timeout_sec);
+void       gd_notification_dismiss     (GdNotification *notification);
+void       gd_notification_set_show_close_button (GdNotification *notification,
+                                                  gboolean show_close_button);
+
+G_END_DECLS
+
+#endif /* _GD_NOTIFICATION_H_ */
diff --git a/src/gnome-software.ui b/src/gnome-software.ui
index 52755ee..b2e622a 100644
--- a/src/gnome-software.ui
+++ b/src/gnome-software.ui
@@ -276,85 +276,189 @@
         </child>
 
         <child>
-          <object class="GtkStack" id="stack_main">
+          <object class="GtkOverlay" id="overlay">
             <property name="visible">True</property>
-            <child>
-              <object class="GsShellOverview" id="shell_overview">
-                <property name="visible">True</property>
-              </object>
-              <packing>
-                <property name="name">overview</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GsShellInstalled" id="shell_installed">
-                <property name="visible">True</property>
-              </object>
-              <packing>
-                <property name="name">installed</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GsShellModerate" id="shell_moderate">
-                <property name="visible">True</property>
-              </object>
-              <packing>
-                <property name="name">moderate</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GsShellLoading" id="shell_loading">
+            <property name="halign">fill</property>
+            <property name="valign">fill</property>
+            <child type="overlay">
+              <object class="GtkEventBox" id="label_installed">
+                <property name="no_show_all">True</property>
                 <property name="visible">True</property>
-              </object>
-              <packing>
-                <property name="name">loading</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GsShellSearch" id="shell_search">
-                <property name="visible">True</property>
-              </object>
-              <packing>
-                <property name="name">search</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GsShellUpdates" id="shell_updates">
-                <property name="visible">True</property>
-              </object>
-              <packing>
-                <property name="name">updates</property>
-              </packing>
-            </child>
+                <property name="visible_window">True</property>
+                <child>
 
+                  <object class="GdNotification" id="notification_event">
+                    <property name="app_paintable">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="show_close_button">True</property>
 
-            <child>
-              <object class="GsShellDetails" id="shell_details">
-                <property name="visible">True</property>
-              </object>
-              <packing>
-                <property name="name">details</property>
-              </packing>
-            </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="orientation">horizontal</property>
+                        <property name="spacing">24</property>
+                        <property name="visible">True</property>
+
+                        <child>
+                          <object class="GtkButtonBox">
+                            <property name="can_focus">False</property>
+                            <property name="layout_style">end</property>
+                            <property name="visible">True</property>
+                            <child>
+                              <object class="GtkButton" id="button_events_sources">
+                                <property name="label" translatable="yes" comments="button in the info 
bar">Software Sources</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="can_focus">False</property>
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">6</property>
+                            <property name="border_width">12</property>
+                            <property name="visible">True</property>
+                            <child>
+                              <object class="GtkLabel" id="label_events_title">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">start</property>
+                                <property name="label">Some Title</property>
+                                <attributes>
+                                  <attribute name="weight" value="bold"/>
+                                </attributes>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="label_events">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">start</property>
+                                <property name="label">Longer text that explains things some more.</property>
+                                <property name="wrap">True</property>
+                                <property name="wrap_mode">word-char</property>
+                                <property name="xalign">0</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">False</property>
+                                <property name="position">1</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                      </object>
 
+                    </child>
+                  </object>
 
-            <child>
-              <object class="GsShellCategory" id="shell_category">
-                <property name="visible">True</property>
+                </child>
               </object>
-              <packing>
-                <property name="name">category</property>
-              </packing>
             </child>
+
             <child>
-              <object class="GsShellExtras" id="shell_extras">
+              <object class="GtkStack" id="stack_main">
                 <property name="visible">True</property>
+                <child>
+                  <object class="GsShellOverview" id="shell_overview">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="name">overview</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GsShellInstalled" id="shell_installed">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="name">installed</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GsShellModerate" id="shell_moderate">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="name">moderate</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GsShellLoading" id="shell_loading">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="name">loading</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GsShellSearch" id="shell_search">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="name">search</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GsShellUpdates" id="shell_updates">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="name">updates</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GsShellDetails" id="shell_details">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="name">details</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GsShellCategory" id="shell_category">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="name">category</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GsShellExtras" id="shell_extras">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="name">extras</property>
+                  </packing>
+                </child>
               </object>
-              <packing>
-                <property name="name">extras</property>
-              </packing>
+
             </child>
           </object>
+
           <packing>
             <property name="expand">True</property>
             <property name="fill">True</property>
diff --git a/src/gs-app-private.h b/src/gs-app-private.h
index 9fb8391..0a83c81 100644
--- a/src/gs-app-private.h
+++ b/src/gs-app-private.h
@@ -26,9 +26,6 @@
 
 G_BEGIN_DECLS
 
-GError         *gs_app_get_last_error          (GsApp          *app);
-void            gs_app_set_last_error          (GsApp          *app,
-                                                GError         *error);
 void            gs_app_set_priority            (GsApp          *app,
                                                 guint           priority);
 guint           gs_app_get_priority            (GsApp          *app);
diff --git a/src/gs-app.c b/src/gs-app.c
index 5d57ec0..654e2cc 100644
--- a/src/gs-app.c
+++ b/src/gs-app.c
@@ -75,7 +75,6 @@ struct _GsApp
        gchar                   *summary_missing;
        gchar                   *description;
        GsAppQuality             description_quality;
-       GError                  *last_error;
        GPtrArray               *screenshots;
        GPtrArray               *categories;
        GPtrArray               *key_colors;
@@ -253,8 +252,6 @@ gs_app_to_string (GsApp *app)
        str = g_string_new ("GsApp:");
        g_string_append_printf (str, " [%p]\n", app);
        gs_app_kv_lpad (str, "kind", as_app_kind_to_string (app->kind));
-       if (app->last_error != NULL)
-               gs_app_kv_lpad (str, "last-error", app->last_error->message);
        gs_app_kv_lpad (str, "state", as_app_state_to_string (app->state));
        if (app->quirk > 0) {
                g_autofree gchar *qstr = _as_app_quirk_to_string (app->quirk);
@@ -793,9 +790,6 @@ gs_app_set_state_internal (GsApp *app, AsAppState state)
                                 app->id, as_app_state_to_string (state));
                        app->state_recover = state;
                }
-
-               /* clear the error as the application has changed state */
-               g_clear_error (&app->last_error);
                break;
        }
 
@@ -3185,38 +3179,6 @@ gs_app_get_priority (GsApp *app)
        return app->priority;
 }
 
-/**
- * gs_app_get_last_error:
- * @app: a #GsApp
- *
- * Get the last error.
- *
- * Returns: a #GError, or %NULL
- *
- * Since: 3.22
- **/
-GError *
-gs_app_get_last_error (GsApp *app)
-{
-       return app->last_error;
-}
-
-/**
- * gs_app_set_last_error:
- * @app: a #GsApp
- * @error: a #GError
- *
- * Sets the last error.
- *
- * Since: 3.22
- **/
-void
-gs_app_set_last_error (GsApp *app, GError *error)
-{
-       g_clear_error (&app->last_error);
-       app->last_error = g_error_copy (error);
-}
-
 static void
 gs_app_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
 {
@@ -3365,8 +3327,6 @@ gs_app_finalize (GObject *object)
        g_ptr_array_unref (app->key_colors);
        if (app->keywords != NULL)
                g_ptr_array_unref (app->keywords);
-       if (app->last_error != NULL)
-               g_error_free (app->last_error);
        if (app->local_file != NULL)
                g_object_unref (app->local_file);
        if (app->pixbuf != NULL)
diff --git a/src/gs-application.c b/src/gs-application.c
index 8e43970..2b874fa 100644
--- a/src/gs-application.c
+++ b/src/gs-application.c
@@ -35,6 +35,8 @@
 #include <gtk/gtkx.h>
 #endif
 
+#include "gd-notification.h"
+
 #ifdef HAVE_PACKAGEKIT
 #include "gs-dbus-helper.h"
 #endif
@@ -301,6 +303,9 @@ gs_application_initialize_ui (GsApplication *app)
 
        initialized = TRUE;
 
+       /* register ahead of loading the .ui file */
+       gd_notification_get_type ();
+
        /* get CSS */
        app->provider = gtk_css_provider_new ();
        gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
diff --git a/src/gs-page.c b/src/gs-page.c
index 9a501b7..8e72b29 100644
--- a/src/gs-page.c
+++ b/src/gs-page.c
@@ -128,7 +128,6 @@ gs_page_app_installed_cb (GObject *source,
                           gpointer user_data)
 {
        g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data;
-       GError *last_error;
        GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
        GsPage *page = helper->page;
        GsPagePrivate *priv = gs_page_get_instance_private (page);
@@ -176,19 +175,6 @@ gs_page_app_installed_cb (GObject *source,
                return;
        }
 
-       /* non-fatal error */
-       last_error = gs_app_get_last_error (helper->app);
-       if (last_error != NULL) {
-               g_warning ("failed to install %s: %s",
-                          gs_app_get_id (helper->app),
-                          last_error->message);
-               gs_app_notify_failed_modal (helper->app,
-                                           gs_shell_get_window (priv->shell),
-                                           GS_PLUGIN_ACTION_INSTALL,
-                                           last_error);
-               return;
-       }
-
        /* only show this if the window is not active */
        if (gs_app_get_state (helper->app) != AS_APP_STATE_QUEUED_FOR_INSTALL &&
            !gs_shell_is_active (priv->shell))
@@ -204,7 +190,6 @@ gs_page_app_removed_cb (GObject *source,
                         gpointer user_data)
 {
        g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data;
-       GError *last_error;
        GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
        GsPage *page = helper->page;
        GsPagePrivate *priv = gs_page_get_instance_private (page);
@@ -250,19 +235,6 @@ gs_page_app_removed_cb (GObject *source,
                return;
        }
 
-       /* non-fatal error */
-       last_error = gs_app_get_last_error (helper->app);
-       if (last_error != NULL) {
-               g_warning ("failed to remove %s: %s",
-                          gs_app_get_id (helper->app),
-                          last_error->message);
-               gs_app_notify_failed_modal (helper->app,
-                                           gs_shell_get_window (priv->shell),
-                                           GS_PLUGIN_ACTION_REMOVE,
-                                           last_error);
-               return;
-       }
-
        if (GS_PAGE_GET_CLASS (page)->app_removed != NULL)
                GS_PAGE_GET_CLASS (page)->app_removed (page, helper->app);
 }
diff --git a/src/gs-plugin-event.c b/src/gs-plugin-event.c
new file mode 100644
index 0000000..fa8cde9
--- /dev/null
+++ b/src/gs-plugin-event.c
@@ -0,0 +1,304 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/**
+ * SECTION:gs-event
+ * @title: GsPluginEvent
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: Infomation about a plugin event
+ *
+ * These functions provide a way for plugins to tell the UI layer about events
+ * that may require displaying to the user. Plugins should not assume that a
+ * specific event is actually shown to the user as it may be ignored
+ * automatically.
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#include "gs-plugin-private.h"
+#include "gs-plugin-event.h"
+
+struct _GsPluginEvent
+{
+       GObject                  parent_instance;
+       GsApp                   *app;
+       GsPluginAction           action;
+       GError                  *gerror;
+       GsPluginEventFlag        flags;
+       gchar                   *unique_id;
+};
+
+G_DEFINE_TYPE (GsPluginEvent, gs_plugin_event, G_TYPE_OBJECT)
+
+/**
+ * gs_plugin_event_set_app:
+ * @event: A #GsPluginEvent
+ * @app: A #GsApp
+ *
+ * Set the application (or source, or whatever component) that caused the event
+ * to be created.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_set_app (GsPluginEvent *event, GsApp *app)
+{
+       g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+       g_return_if_fail (GS_IS_APP (app));
+       g_set_object (&event->app, app);
+}
+
+/**
+ * gs_plugin_event_get_app:
+ * @event: A #GsPluginEvent
+ *
+ * Gets an application that created the event.
+ *
+ * Returns: (transfer none): a #GsApp, or %NULL if unset
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_plugin_event_get_app (GsPluginEvent *event)
+{
+       g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), NULL);
+       return event->app;
+}
+
+/**
+ * gs_plugin_event_set_action:
+ * @event: A #GsPluginEvent
+ * @app: A #GsPluginAction, e.g. %GS_PLUGIN_ACTION_UPDATE
+ *
+ * Set the action that caused the event to be created.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_set_action (GsPluginEvent *event, GsPluginAction action)
+{
+       g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+       event->action = action;
+}
+
+/**
+ * gs_plugin_event_get_action:
+ * @event: A #GsPluginEvent
+ *
+ * Gets an action that created the event.
+ *
+ * Returns: (transfer none): a #GsPluginAction, e.g. %GS_PLUGIN_ACTION_UPDATE
+ *
+ * Since: 3.22
+ **/
+GsPluginAction
+gs_plugin_event_get_action (GsPluginEvent *event)
+{
+       g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), 0);
+       return event->action;
+}
+
+/**
+ * gs_plugin_event_get_unique_id:
+ * @event: A #GsPluginEvent
+ *
+ * Gets the unique ID for the event. In most cases (if an app has been set)
+ * this will just be the actual #GsApp unique-id. In the cases where only error
+ * has been set a virtual (but plausible) ID will be generated.
+ *
+ * Returns: a string, or %NULL for invalid
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_event_get_unique_id (GsPluginEvent *event)
+{
+       /* just proxy */
+       if (event->app != NULL)
+               return gs_app_get_unique_id (event->app);
+
+       /* generate from error */
+       if (event->gerror != NULL) {
+               if (event->unique_id == NULL) {
+                       g_autofree gchar *id = NULL;
+                       id = g_strdup_printf ("%s.error",
+                                             gs_plugin_error_to_string (event->gerror->code));
+                       event->unique_id = as_utils_unique_id_build (AS_APP_SCOPE_UNKNOWN,
+                                                                    AS_BUNDLE_KIND_UNKNOWN,
+                                                                    NULL,
+                                                                    AS_APP_KIND_UNKNOWN,
+                                                                    id,
+                                                                    NULL);
+               }
+               return event->unique_id;
+       }
+
+       /* failed */
+       return NULL;
+}
+
+/**
+ * gs_plugin_event_get_kind:
+ * @event: A #GsPluginEvent
+ * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID
+ *
+ * Adds a flag to the event.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_add_flag (GsPluginEvent *event, GsPluginEventFlag flag)
+{
+       g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+       event->flags |= flag;
+}
+
+/**
+ * gs_plugin_event_set_kind:
+ * @event: A #GsPluginEvent
+ * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID
+ *
+ * Removes a flag from the event.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_remove_flag (GsPluginEvent *event, GsPluginEventFlag flag)
+{
+       g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+       event->flags &= ~flag;
+}
+
+/**
+ * gs_plugin_event_has_flag:
+ * @event: A #GsPluginEvent
+ * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID
+ *
+ * Finds out if the event has a specific flag.
+ *
+ * Returns: %TRUE if the flag is set
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_event_has_flag (GsPluginEvent *event, GsPluginEventFlag flag)
+{
+       g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), FALSE);
+       return (event->flags & flag > 0);
+}
+
+/**
+ * gs_plugin_event_set_error:
+ * @event: A #GsPluginEvent
+ * @code: A #GsPluginError, e.g. %GS_PLUGIN_ERROR_NO_NETWORK
+ * @message: A error message
+ *
+ * Sets the event error.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_set_error (GsPluginEvent *event, gint code, const gchar *message)
+{
+       g_clear_error (&event->gerror);
+       g_set_error_literal (&event->gerror,
+                            GS_PLUGIN_ERROR,
+                            code,
+                            message);
+}
+
+/**
+ * gs_plugin_event_set_gerror:
+ * @event: A #GsPluginEvent
+ * @error: A #GError
+ *
+ * Sets the event error.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_set_gerror (GsPluginEvent *event, const GError *error)
+{
+       g_clear_error (&event->gerror);
+       event->gerror = g_error_copy (error);
+}
+
+/**
+ * gs_plugin_event_get_gerror:
+ * @event: A #GsPluginEvent
+ *
+ * Gets the event error.
+ *
+ * Returns: a #GError, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const GError *
+gs_plugin_event_get_gerror (GsPluginEvent *event)
+{
+       return event->gerror;
+}
+
+static void
+gs_plugin_event_finalize (GObject *object)
+{
+       GsPluginEvent *event = GS_PLUGIN_EVENT (object);
+       if (event->gerror != NULL)
+               g_error_free (event->gerror);
+       if (event->app != NULL)
+               g_object_unref (event->app);
+       g_free (event->unique_id);
+       G_OBJECT_CLASS (gs_plugin_event_parent_class)->finalize (object);
+}
+
+static void
+gs_plugin_event_class_init (GsPluginEventClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = gs_plugin_event_finalize;
+}
+
+static void
+gs_plugin_event_init (GsPluginEvent *event)
+{
+}
+
+/**
+ * gs_plugin_event_new:
+ *
+ * Creates a new event.
+ *
+ * Returns: A newly allocated #GsPluginEvent
+ *
+ * Since: 3.22
+ **/
+GsPluginEvent *
+gs_plugin_event_new (void)
+{
+       GsPluginEvent *event;
+       event = g_object_new (GS_TYPE_PLUGIN_EVENT, NULL);
+       return GS_PLUGIN_EVENT (event);
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-plugin-event.h b/src/gs-plugin-event.h
new file mode 100644
index 0000000..f3c0a1a
--- /dev/null
+++ b/src/gs-plugin-event.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_PLUGIN_EVENT
+#define __GS_PLUGIN_EVENT
+
+#include <glib-object.h>
+
+#include "gs-app.h"
+#include "gs-plugin-private.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_EVENT (gs_plugin_event_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginEvent, gs_plugin_event, GS, PLUGIN_EVENT, GObject)
+
+/**
+ * GsPluginEventFlag:
+ * @GS_PLUGIN_EVENT_FLAG_NONE:         No special flags set
+ * @GS_PLUGIN_EVENT_FLAG_INVALID:      Event is no longer valid, e.g. was dismissed
+ * @GS_PLUGIN_EVENT_FLAG_VISIBLE:      Event is is visible on the screen
+ * @GS_PLUGIN_EVENT_FLAG_WARNING:      Event should be shown with more urgency
+ *
+ * Any flags an event can have.
+ **/
+typedef enum {
+       GS_PLUGIN_EVENT_FLAG_NONE       = 0,            /* Since: 3.22 */
+       GS_PLUGIN_EVENT_FLAG_INVALID    = 1 << 0,       /* Since: 3.22 */
+       GS_PLUGIN_EVENT_FLAG_VISIBLE    = 1 << 1,       /* Since: 3.22 */
+       GS_PLUGIN_EVENT_FLAG_WARNING    = 1 << 2,       /* Since: 3.22 */
+       /*< private >*/
+       GS_PLUGIN_EVENT_FLAG_LAST
+} GsPluginEventFlag;
+
+GsPluginEvent          *gs_plugin_event_new            (void);
+
+const gchar            *gs_plugin_event_get_unique_id  (GsPluginEvent          *event);
+
+void                    gs_plugin_event_set_app        (GsPluginEvent          *event,
+                                                        GsApp                  *app);
+GsApp                  *gs_plugin_event_get_app        (GsPluginEvent          *event);
+void                    gs_plugin_event_set_action     (GsPluginEvent          *event,
+                                                        GsPluginAction          action);
+GsPluginAction          gs_plugin_event_get_action     (GsPluginEvent          *event);
+
+void                    gs_plugin_event_set_error      (GsPluginEvent          *event,
+                                                        gint                    code,
+                                                        const gchar            *message);
+void                    gs_plugin_event_set_gerror     (GsPluginEvent          *event,
+                                                        const GError           *error);
+const GError           *gs_plugin_event_get_gerror     (GsPluginEvent          *event);
+
+void                    gs_plugin_event_add_flag       (GsPluginEvent          *event,
+                                                        GsPluginEventFlag       flag);
+void                    gs_plugin_event_remove_flag    (GsPluginEvent          *event,
+                                                        GsPluginEventFlag       flag);
+gboolean                gs_plugin_event_has_flag       (GsPluginEvent          *event,
+                                                        GsPluginEventFlag       flag);
+
+G_END_DECLS
+
+#endif /* __GS_PLUGIN_EVENT */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-plugin-loader.c b/src/gs-plugin-loader.c
index 738d1d0..61c35a5 100644
--- a/src/gs-plugin-loader.c
+++ b/src/gs-plugin-loader.c
@@ -30,6 +30,7 @@
 #include "gs-category-private.h"
 #include "gs-plugin-loader.h"
 #include "gs-plugin.h"
+#include "gs-plugin-event.h"
 #include "gs-plugin-private.h"
 #include "gs-common.h"
 
@@ -52,6 +53,7 @@ typedef struct
        GPtrArray               *pending_apps;
 
        GSettings               *settings;
+       GHashTable              *events_by_id;          /* unique-id : GsPluginEvent */
 
        gchar                   **compatible_projects;
        guint                    scale;
@@ -71,6 +73,12 @@ enum {
        SIGNAL_LAST
 };
 
+enum {
+       PROP_0,
+       PROP_EVENTS,
+       PROP_LAST
+};
+
 static guint signals [SIGNAL_LAST] = { 0 };
 
 typedef void            (*GsPluginFunc)                (GsPlugin       *plugin);
@@ -898,17 +906,53 @@ gs_plugin_loader_get_app_is_compatible (GsApp *app, gpointer user_data)
 }
 
 static void
-gs_plugin_loader_set_app_error (GsApp *app, GError *error)
+gs_plugin_loader_add_event (GsPluginLoader *plugin_loader, GsPluginEvent *event)
+{
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+       g_hash_table_insert (priv->events_by_id,
+                            (gpointer) gs_plugin_event_get_unique_id (event),
+                            g_object_ref (event));
+       g_object_notify (G_OBJECT (plugin_loader), "events");
+}
+
+/**
+ * gs_plugin_loader_get_event_by_id:
+ * @list: A #GsAppList
+ * @unique_id: A unique_id
+ *
+ * Finds the first matching event in the list using the usual wildcard
+ * rules allowed in unique_ids.
+ *
+ * Returns: (transfer none): a #GsPluginEvent, or %NULL if not found
+ **/
+GsPluginEvent *
+gs_plugin_loader_get_event_by_id (GsPluginLoader *plugin_loader, const gchar *unique_id)
+{
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+       return g_hash_table_lookup (priv->events_by_id, unique_id);
+}
+
+static void
+gs_plugin_loader_set_app_error (GsPluginLoader *plugin_loader,
+                               GsPluginAction action,
+                               GsApp *app, GError *error)
 {
        if (error == NULL)
                return;
 
        /* random, non-plugin error domains are never shown to the user */
        if (error->domain == GS_PLUGIN_ERROR) {
+               g_autoptr(GsPluginEvent) event = gs_plugin_event_new ();
                g_debug ("saving error for %s: %s",
                         gs_app_get_unique_id (app),
                         error->message);
-               gs_app_set_last_error (app, error);
+
+               /* create and add event */
+               gs_plugin_event_set_action (event, action);
+               gs_plugin_event_set_app (event, app);
+               gs_plugin_event_set_gerror (event, error);
+               gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+               gs_plugin_loader_add_event (plugin_loader, event);
        } else {
                g_warning ("not saving error for %s: %s",
                           gs_app_get_unique_id (app),
@@ -929,6 +973,7 @@ gs_plugin_loader_is_auth_error (GError *err)
 static gboolean
 gs_plugin_loader_run_action (GsPluginLoader *plugin_loader,
                             GsApp *app,
+                            GsPluginAction action,
                             const gchar *function_name,
                             GCancellable *cancellable,
                             GError **error)
@@ -984,7 +1029,8 @@ gs_plugin_loader_run_action (GsPluginLoader *plugin_loader,
                                   function_name,
                                   gs_plugin_get_name (plugin),
                                   error_local->message);
-                       gs_plugin_loader_set_app_error (app, error_local);
+                       gs_plugin_loader_set_app_error (plugin_loader, action,
+                                                       app, error_local);
                        continue;
                }
                anything_ran = TRUE;
@@ -2730,6 +2776,7 @@ gs_plugin_loader_app_action_thread_cb (GTask *task,
        /* perform action */
        ret = gs_plugin_loader_run_action (plugin_loader,
                                           state->app,
+                                          state->action,
                                           state->function_name,
                                           cancellable,
                                           &error);
@@ -3373,6 +3420,81 @@ gs_plugin_loader_get_enabled (GsPluginLoader *plugin_loader,
        return gs_plugin_get_enabled (plugin);
 }
 
+/**
+ * gs_plugin_loader_get_events:
+ * @plugin_loader: A #GsPluginLoader
+ *
+ * Gets all plugin events, even ones that are not active or visible anymore.
+ *
+ * Returns: (transfer container) (element-type GsPluginEvent): events
+ **/
+GPtrArray *
+gs_plugin_loader_get_events (GsPluginLoader *plugin_loader)
+{
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+       GPtrArray *events = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+       GList *l;
+       g_autoptr(GList) keys = NULL;
+
+       /* just add everything */
+       keys = g_hash_table_get_keys (priv->events_by_id);
+       for (l = keys; l != NULL; l = l->next) {
+               const gchar *key = l->data;
+               GsPluginEvent *event = g_hash_table_lookup (priv->events_by_id, key);
+               g_ptr_array_add (events, g_object_ref (event));
+       }
+       return events;
+}
+
+/**
+ * gs_plugin_loader_get_event_default:
+ * @plugin_loader: A #GsPluginLoader
+ *
+ * Gets an active plugin event where active means that it was not been
+ * already dismissed by the user.
+ *
+ * Returns: a #GsPluginEvent, or %NULL for none
+ **/
+GsPluginEvent *
+gs_plugin_loader_get_event_default (GsPluginLoader *plugin_loader)
+{
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+       GList *l;
+       g_autoptr(GList) keys = NULL;
+
+       /* just add everything */
+       keys = g_hash_table_get_keys (priv->events_by_id);
+       for (l = keys; l != NULL; l = l->next) {
+               const gchar *key = l->data;
+               GsPluginEvent *event = g_hash_table_lookup (priv->events_by_id, key);
+               if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID))
+                       return event;
+       }
+       return NULL;
+}
+
+/**
+ * gs_plugin_loader_remove_events:
+ * @plugin_loader: A #GsPluginLoader
+ *
+ * Removes all plugin events from the loader. This function should only be
+ * called from the self tests.
+ **/
+void
+gs_plugin_loader_remove_events (GsPluginLoader *plugin_loader)
+{
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+       g_hash_table_remove_all (priv->events_by_id);
+}
+
+static void
+gs_plugin_loader_add_event_cb (GsPlugin *plugin,
+                              GsPluginEvent *event,
+                              GsPluginLoader *plugin_loader)
+{
+       gs_plugin_loader_add_event (plugin_loader, event);
+}
+
 static void
 gs_plugin_loader_status_changed_cb (GsPlugin *plugin,
                                    GsApp *app,
@@ -3482,6 +3604,9 @@ gs_plugin_loader_open_plugin (GsPluginLoader *plugin_loader,
        g_signal_connect (plugin, "status-changed",
                          G_CALLBACK (gs_plugin_loader_status_changed_cb),
                          plugin_loader);
+       g_signal_connect (plugin, "add-event",
+                         G_CALLBACK (gs_plugin_loader_add_event_cb),
+                         plugin_loader);
        gs_plugin_set_soup_session (plugin, priv->soup_session);
        gs_plugin_set_auth_array (plugin, priv->auth_array);
        gs_plugin_set_profile (plugin, priv->profile);
@@ -3852,6 +3977,34 @@ gs_plugin_loader_dump_state (GsPluginLoader *plugin_loader)
 }
 
 static void
+gs_plugin_loader_get_property (GObject *object, guint prop_id,
+                              GValue *value, GParamSpec *pspec)
+{
+       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+       switch (prop_id) {
+       case PROP_EVENTS:
+               g_value_set_pointer (value, priv->events_by_id);
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gs_plugin_loader_set_property (GObject *object, guint prop_id,
+                              const GValue *value, GParamSpec *pspec)
+{
+       switch (prop_id) {
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
 gs_plugin_loader_dispose (GObject *object)
 {
        GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
@@ -3885,6 +4038,7 @@ gs_plugin_loader_finalize (GObject *object)
        g_free (priv->locale);
        g_free (priv->language);
        g_object_unref (priv->global_cache);
+       g_hash_table_unref (priv->events_by_id);
 
        g_mutex_clear (&priv->pending_apps_mutex);
 
@@ -3894,11 +4048,19 @@ gs_plugin_loader_finalize (GObject *object)
 static void
 gs_plugin_loader_class_init (GsPluginLoaderClass *klass)
 {
+       GParamSpec *pspec;
        GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
+       object_class->get_property = gs_plugin_loader_get_property;
+       object_class->set_property = gs_plugin_loader_set_property;
        object_class->dispose = gs_plugin_loader_dispose;
        object_class->finalize = gs_plugin_loader_finalize;
 
+       pspec = g_param_spec_string ("events", NULL, NULL,
+                                    NULL,
+                                    G_PARAM_READABLE);
+       g_object_class_install_property (object_class, PROP_EVENTS, pspec);
+
        signals [SIGNAL_STATUS_CHANGED] =
                g_signal_new ("status-changed",
                              G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
@@ -3943,6 +4105,18 @@ gs_plugin_loader_init (GsPluginLoader *plugin_loader)
        priv->profile = as_profile_new ();
        priv->settings = g_settings_new ("org.gnome.software");
 
+#if AS_CHECK_VERSION(0,6,2)
+       priv->events_by_id = g_hash_table_new_full ((GHashFunc) as_utils_unique_id_hash,
+                                                   (GEqualFunc) as_utils_unique_id_equal,
+                                                   NULL,
+                                                   (GDestroyNotify) g_object_unref);
+#else
+       priv->events_by_id = g_hash_table_new_full (g_str_hash,
+                                                   g_str_equal,
+                                                   NULL,
+                                                   (GDestroyNotify) g_object_unref);
+#endif
+
        /* share a soup session (also disable the double-compression) */
        priv->soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, gs_user_agent (),
                                                            SOUP_SESSION_TIMEOUT, 10,
@@ -4059,6 +4233,52 @@ gs_plugin_loader_set_network_status (GsPluginLoader *plugin_loader,
 
 /******************************************************************************/
 
+/* if the error is worthy of notifying the user and the unique-id is found in
+ * the error message then automatically create a plugin event for the plugin */
+static void
+gs_plugin_loader_create_event_from_error (GsPluginLoader *plugin_loader,
+                                         GsPlugin *plugin, const GError *error)
+{
+       guint i;
+       g_autoptr(GsApp) app = NULL;
+       g_auto(GStrv) split = NULL;
+       g_autoptr(GsPluginEvent) event = gs_plugin_event_new ();
+
+       /* not us */
+       if (error->domain != GS_PLUGIN_ERROR)
+               return;
+
+       /* only create events for some error domains */
+       switch (error->code) {
+       case GS_PLUGIN_ERROR_DOWNLOAD_FAILED:
+               break;
+       default:
+               return;
+       }
+
+       /* can we find a unique ID */
+       split = g_strsplit_set (error->message, "[]: ", -1);
+       for (i = 0; split[i] != NULL; i++) {
+               if (as_utils_unique_id_valid (split[i])) {
+                       app = gs_plugin_cache_lookup (plugin, split[i]);
+                       if (app != NULL)
+                               break;
+               }
+       }
+
+       /* no app :( */
+       if (app == NULL) {
+               g_debug ("no unique ID found for %s", error->message);
+               return;
+       }
+
+       /* create plugin event */
+       g_debug ("found %s in error", gs_app_get_unique_id (app));
+       gs_plugin_event_set_app (event, app);
+       gs_plugin_event_set_gerror (event, error);
+       gs_plugin_loader_add_event (plugin_loader, event);
+}
+
 static gboolean
 gs_plugin_loader_run_refresh (GsPluginLoader *plugin_loader,
                              guint cache_age,
@@ -4111,6 +4331,10 @@ gs_plugin_loader_run_refresh (GsPluginLoader *plugin_loader,
                                g_propagate_error (error, error_local);
                                error_local = NULL;
                                return FALSE;
+                       } else {
+                               gs_plugin_loader_create_event_from_error (plugin_loader,
+                                                                         plugin,
+                                                                         error_local);
                        }
                        g_warning ("failed to call %s on %s: %s",
                                   function_name,
@@ -4509,7 +4733,10 @@ gs_plugin_loader_update_thread_cb (GTask *task,
                                           function_name,
                                           gs_plugin_get_name (plugin),
                                           error_local->message);
-                               gs_plugin_loader_set_app_error (app, error_local);
+                               gs_plugin_loader_set_app_error (plugin_loader,
+                                                               state->action,
+                                                               app,
+                                                               error_local);
                                continue;
                        }
                }
diff --git a/src/gs-plugin-loader.h b/src/gs-plugin-loader.h
index c43a2dd..6a977ca 100644
--- a/src/gs-plugin-loader.h
+++ b/src/gs-plugin-loader.h
@@ -27,6 +27,7 @@
 #include "gs-app.h"
 #include "gs-auth.h"
 #include "gs-category.h"
+#include "gs-plugin-event.h"
 #include "gs-plugin-private.h"
 
 G_BEGIN_DECLS
@@ -234,6 +235,12 @@ void                gs_plugin_loader_set_network_status    (GsPluginLoader 
*plugin_loader,
 gboolean        gs_plugin_loader_get_plugin_supported  (GsPluginLoader *plugin_loader,
                                                         const gchar    *plugin_func);
 
+GPtrArray      *gs_plugin_loader_get_events            (GsPluginLoader *plugin_loader);
+GsPluginEvent  *gs_plugin_loader_get_event_by_id       (GsPluginLoader *plugin_loader,
+                                                        const gchar    *unique_id);
+GsPluginEvent  *gs_plugin_loader_get_event_default     (GsPluginLoader *plugin_loader);
+void            gs_plugin_loader_remove_events         (GsPluginLoader *plugin_loader);
+
 G_END_DECLS
 
 #endif /* __GS_PLUGIN_LOADER_H */
diff --git a/src/gs-plugin-vfuncs.h b/src/gs-plugin-vfuncs.h
index 525a2ae..2205320 100644
--- a/src/gs-plugin-vfuncs.h
+++ b/src/gs-plugin-vfuncs.h
@@ -513,8 +513,7 @@ gboolean     gs_plugin_update_cancel                (GsPlugin       *plugin,
  * to complete.
  *
  * On failure the error message returned will usually only be shown on the
- * console, but it may also be retained on the #GsApp object.
- * The UI code can retrieve the error using gs_app_get_last_error().
+ * console, but they can also be retrieved using gs_plugin_loader_get_events().
  *
  * NOTE: Once the action is complete, the plugin must set the new state of @app
  * to %AS_APP_STATE_INSTALLED.
@@ -543,8 +542,7 @@ gboolean     gs_plugin_app_install                  (GsPlugin       *plugin,
  * to complete.
  *
  * On failure the error message returned will usually only be shown on the
- * console, but it may also be retained on the #GsApp object.
- * The UI code can retrieve the error using gs_app_get_last_error().
+ * console, but they can also be retrieved using gs_plugin_loader_get_events().
  *
  * NOTE: Once the action is complete, the plugin must set the new state of @app
  * to %AS_APP_STATE_AVAILABLE or %AS_APP_STATE_UNKNOWN if not known.
@@ -591,8 +589,7 @@ gboolean     gs_plugin_app_set_rating               (GsPlugin       *plugin,
  * to complete.
  *
  * On failure the error message returned will usually only be shown on the
- * console, but it may also be retained on the #GsApp object.
- * The UI code can retrieve the error using gs_app_get_last_error().
+ * console, but they can also be retrieved using gs_plugin_loader_get_events().
  *
  * NOTE: Once the action is complete, the plugin must set the new state of @app
  * to %AS_APP_STATE_INSTALLED or %AS_APP_STATE_UNKNOWN if not known.
diff --git a/src/gs-plugin.c b/src/gs-plugin.c
index f83c64c..ed237e3 100644
--- a/src/gs-plugin.c
+++ b/src/gs-plugin.c
@@ -48,6 +48,7 @@
 #include <gdk/gdk.h>
 
 #include "gs-os-release.h"
+#include "gs-plugin-event.h"
 #include "gs-plugin-private.h"
 #include "gs-plugin.h"
 #include "gs-utils.h"
@@ -90,6 +91,7 @@ enum {
        SIGNAL_UPDATES_CHANGED,
        SIGNAL_STATUS_CHANGED,
        SIGNAL_RELOAD,
+       SIGNAL_ADD_EVENT,
        SIGNAL_LAST
 };
 
@@ -850,6 +852,7 @@ typedef struct {
        GsPlugin        *plugin;
        GsApp           *app;
        GsPluginStatus   status;
+       GsPluginEvent   *event;
        guint            percentage;
 } GsPluginStatusHelper;
 
@@ -891,6 +894,38 @@ gs_plugin_status_update (GsPlugin *plugin, GsApp *app, GsPluginStatus status)
 }
 
 static gboolean
+gs_plugin_add_event_cb (gpointer user_data)
+{
+       GsPluginStatusHelper *helper = (GsPluginStatusHelper *) user_data;
+       g_signal_emit (helper->plugin,
+                      signals[SIGNAL_ADD_EVENT], 0,
+                      helper->event);
+       g_object_unref (helper->event);
+       g_slice_free (GsPluginStatusHelper, helper);
+       return FALSE;
+}
+
+/**
+ * gs_plugin_add_event:
+ * @plugin: a #GsPlugin
+ * @event: a #GsPluginEvent
+ *
+ * Provide a way for plugins to tell the UI layer about events that may require
+ * displaying to the user. Plugins should not assume that a specific event is
+ * actually shown to the user as it may be ignored automatically.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_add_event (GsPlugin *plugin, GObject *event)
+{
+       GsPluginStatusHelper *helper = g_slice_new0 (GsPluginStatusHelper);
+       helper->plugin = plugin;
+       helper->event = g_object_ref (event);
+       g_idle_add (gs_plugin_add_event_cb, helper);
+}
+
+static gboolean
 gs_plugin_app_launch_cb (gpointer user_data)
 {
        GAppInfo *appinfo = (GAppInfo *) user_data;
@@ -1500,6 +1535,13 @@ gs_plugin_class_init (GsPluginClass *klass)
                              NULL, NULL, g_cclosure_marshal_generic,
                              G_TYPE_NONE, 2, GS_TYPE_APP, G_TYPE_UINT);
 
+       signals [SIGNAL_ADD_EVENT] =
+               g_signal_new ("add-event",
+                             G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+                             G_STRUCT_OFFSET (GsPluginClass, add_event),
+                             NULL, NULL, g_cclosure_marshal_generic,
+                             G_TYPE_NONE, 1, GS_TYPE_PLUGIN_EVENT);
+
        signals [SIGNAL_RELOAD] =
                g_signal_new ("reload",
                              G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
diff --git a/src/gs-plugin.h b/src/gs-plugin.h
index 43c2d0b..02f73e6 100644
--- a/src/gs-plugin.h
+++ b/src/gs-plugin.h
@@ -47,7 +47,9 @@ struct _GsPluginClass
                                                         GsApp          *app,
                                                         guint           status);
        void                    (*reload)               (GsPlugin       *plugin);
-       gpointer                 padding[28];
+       void                    (*add_event)            (GsPlugin       *plugin,
+                                                        GObject        *event);
+       gpointer                 padding[27];
 };
 
 typedef struct GsPluginData    GsPluginData;
@@ -305,6 +307,8 @@ void                 gs_plugin_reload                       (GsPlugin       *plugin);
 const gchar    *gs_plugin_status_to_string             (GsPluginStatus  status);
 void            gs_plugin_error_add_unique_id          (GError         **error,
                                                         GsApp          *app);
+void            gs_plugin_add_event                    (GsPlugin       *plugin,
+                                                        GObject        *event);
 
 G_END_DECLS
 
diff --git a/src/gs-self-test.c b/src/gs-self-test.c
index 1e92c0a..5f78219 100644
--- a/src/gs-self-test.c
+++ b/src/gs-self-test.c
@@ -371,10 +371,15 @@ gs_plugin_loader_install_func (GsPluginLoader *plugin_loader)
 static void
 gs_plugin_loader_error_func (GsPluginLoader *plugin_loader)
 {
+       GsPluginEvent *event;
+       const GError *app_error;
        gboolean ret;
-       g_autoptr(GsApp) app = NULL;
        g_autoptr(GError) error = NULL;
-       GError *last_error;
+       g_autoptr(GPtrArray) events = NULL;
+       g_autoptr(GsApp) app = NULL;
+
+       /* remove previous errors */
+       gs_plugin_loader_remove_events (plugin_loader);
 
        /* suppress this */
        g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
@@ -394,9 +399,25 @@ gs_plugin_loader_error_func (GsPluginLoader *plugin_loader)
        /* ensure we failed the plugin action */
        g_test_assert_expected_messages ();
 
-       /* retrieve the error from the application */
-       last_error = gs_app_get_last_error (app);
-       g_assert_error (last_error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_NETWORK);
+       /* get event by app-id */
+       event = gs_plugin_loader_get_event_by_id (plugin_loader,
+                                                 "*/*/*/*/chiron.desktop/*");
+       g_assert (event != NULL);
+       g_assert (gs_plugin_event_get_app (event) == app);
+
+       /* get last active event */
+       event = gs_plugin_loader_get_event_default (plugin_loader);
+       g_assert (event != NULL);
+       g_assert (gs_plugin_event_get_app (event) == app);
+
+       /* check all the events */
+       events = gs_plugin_loader_get_events (plugin_loader);
+       g_assert_cmpint (events->len, ==, 1);
+       event = g_ptr_array_index (events, 0);
+       g_assert (gs_plugin_event_get_app (event) == app);
+       app_error = gs_plugin_event_get_gerror (event);
+       g_assert (app_error != NULL);
+       g_assert_error (app_error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_NETWORK);
 }
 
 static void
diff --git a/src/gs-shell-updates.c b/src/gs-shell-updates.c
index 366e0ec..8eea596 100644
--- a/src/gs-shell-updates.c
+++ b/src/gs-shell-updates.c
@@ -1004,7 +1004,6 @@ upgrade_download_finished_cb (GObject *source,
                               gpointer user_data)
 {
        GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
-       GError *last_error;
        g_autoptr(GError) error = NULL;
        g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data;
 
@@ -1013,18 +1012,6 @@ upgrade_download_finished_cb (GObject *source,
                        return;
                g_warning ("failed to upgrade-download: %s", error->message);
        }
-
-       last_error = gs_app_get_last_error (helper->app);
-       if (last_error != NULL) {
-               g_warning ("failed to upgrade-download %s: %s",
-                          gs_app_get_id (helper->app),
-                          last_error->message);
-               gs_app_notify_failed_modal (helper->app,
-                                           gs_shell_get_window (helper->self->shell),
-                                           GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD,
-                                           last_error);
-               return;
-       }
 }
 
 static void
diff --git a/src/gs-shell.c b/src/gs-shell.c
index cfd23b7..36f87fb 100644
--- a/src/gs-shell.c
+++ b/src/gs-shell.c
@@ -25,6 +25,8 @@
 #include <string.h>
 #include <glib/gi18n.h>
 
+#include "gd-notification.h"
+
 #include "gs-common.h"
 #include "gs-shell.h"
 #include "gs-shell-details.h"
@@ -413,6 +415,12 @@ save_back_entry (GsShell *shell)
 }
 
 static void
+gs_shell_plugin_events_sources_cb (GtkWidget *widget, GsShell *shell)
+{
+       gs_shell_show_sources (shell);
+}
+
+static void
 gs_shell_back_button_cb (GtkWidget *widget, GsShell *shell)
 {
        GsShellPrivate *priv = gs_shell_get_instance_private (shell);
@@ -614,6 +622,195 @@ gs_shell_monitor_permission (GsShell *shell)
                                  G_CALLBACK (on_permission_changed), shell);
 }
 
+static gboolean
+gs_shell_events_show (GsShell *shell, GsPluginEvent *event)
+{
+       GsApp *app = gs_plugin_event_get_app (event);
+       GsShellPrivate *priv = gs_shell_get_instance_private (shell);
+       GtkWidget *widget;
+       const GError *error;
+       const gchar *tmp;
+       AsAppKind app_kind = AS_APP_KIND_UNKNOWN;
+       g_autofree gchar *str = NULL;
+       g_autofree gchar *title = NULL;
+
+       /* get error */
+       error = gs_plugin_event_get_gerror (event);
+       if (error == NULL)
+               return FALSE;
+
+       if (app != NULL) {
+               app_kind = gs_app_get_kind (app);
+               if (app_kind == AS_APP_KIND_GENERIC) {
+                       title = g_strdup_printf ("“%s” [%s]",
+                                                gs_app_get_origin_ui (app),
+                                                gs_app_get_origin_hostname (app));
+               } else {
+                       title = g_strdup (gs_app_get_name (app));
+               }
+       }
+
+       switch (error->code) {
+       case GS_PLUGIN_ERROR_FAILED:
+               if (app_kind == AS_APP_KIND_SOURCE) {
+                       /* TRANSLATORS: failure text for the in-app notification */
+                       str = g_strdup_printf (_("Failure with source %s"), title);
+               } else if (app_kind == AS_APP_KIND_GENERIC) {
+                       /* TRANSLATORS: failure text for the in-app notification */
+                       str = g_strdup_printf (_("Failure with server %s"), title);
+               } else if (app_kind == AS_APP_KIND_DESKTOP) {
+                       /* TRANSLATORS: failure text for the in-app notification */
+                       str = g_strdup_printf (_("Failure with application %s"), title);
+               } else {
+                       /* TRANSLATORS: failure text for the in-app notification */
+                       str = g_strdup_printf (_("Generic failure"));
+               }
+               break;
+       case GS_PLUGIN_ERROR_NOT_SUPPORTED:
+               break;
+       case GS_PLUGIN_ERROR_NO_NETWORK:
+               break;
+       case GS_PLUGIN_ERROR_NO_SECURITY:
+               break;
+       case GS_PLUGIN_ERROR_NO_SPACE:
+               break;
+       case GS_PLUGIN_ERROR_AUTH_REQUIRED:
+               break;
+       case GS_PLUGIN_ERROR_AUTH_INVALID:
+               break;
+       case GS_PLUGIN_ERROR_PIN_REQUIRED:
+               break;
+       case GS_PLUGIN_ERROR_ACCOUNT_SUSPENDED:
+               break;
+       case GS_PLUGIN_ERROR_ACCOUNT_DEACTIVATED:
+               break;
+       case GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED:
+               break;
+       case GS_PLUGIN_ERROR_DOWNLOAD_FAILED:
+               if (app_kind == AS_APP_KIND_SOURCE) {
+                       /* TRANSLATORS: failure text for the in-app notification */
+                       str = g_strdup_printf (_("Unable to contact %s"), title);
+               } else if (app_kind == AS_APP_KIND_GENERIC) {
+                       /* TRANSLATORS: failure text for the in-app notification */
+                       str = g_strdup_printf (_("Unable to contact %s"), title);
+               } else if (app_kind == AS_APP_KIND_DESKTOP) {
+                       /* TRANSLATORS: failure text for the in-app notification */
+                       str = g_strdup_printf (_("Unable to download %s"), title);
+               } else {
+                       /* TRANSLATORS: failure text for the in-app notification */
+                       str = g_strdup_printf (_("Unable to download"));
+               }
+               break;
+       case GS_PLUGIN_ERROR_WRITE_FAILED:
+               break;
+       case GS_PLUGIN_ERROR_INVALID_FORMAT:
+               break;
+       case GS_PLUGIN_ERROR_DELETE_FAILED:
+               break;
+       default:
+               break;
+       }
+       if (str == NULL)
+               return FALSE;
+
+// https://etherpad.gnome.org/p/409v9S7Q1W
+#if 0
+ * @GS_PLUGIN_ERROR_FAILED:                    Generic failure
+ * @GS_PLUGIN_ERROR_NOT_SUPPORTED:             Action not supported
+ * @GS_PLUGIN_ERROR_CANCELLED:                 Action was cancelled
+ * @GS_PLUGIN_ERROR_NO_NETWORK:                        No network connection available
+ * @GS_PLUGIN_ERROR_NO_SECURITY:               Security policy forbid action
+ * @:                  No disk space to allow action
+ * @:          Authentication was required
+ * @:          Provided authentication was invalid
+ * @:          PIN required for authentication
+ * @:          User account has been suspended
+ * @:  User account has been deactivated
+ * @:  The plugins installed are incompatible
+ * @:          The download action failed
+ * @:          The save-to-disk failed
+ * @:          The data format is invalid
+ * @:          The delete action failed
+#endif
+
+       /* set visible */
+       widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "notification_event"));
+       gtk_widget_set_visible (widget, TRUE);
+
+       /* sources button */
+       widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_sources"));
+       gtk_widget_set_visible (widget,
+                               app != NULL &&
+                               gs_app_get_kind (app) == AS_APP_KIND_SOURCE);
+
+       /* set header */
+       widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "label_events_title"));
+       gtk_label_set_label (GTK_LABEL (widget), str);
+       gtk_widget_set_visible (widget, TRUE);
+
+       /* fill in detail */
+       widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "label_events"));
+       error = gs_plugin_event_get_gerror (event);
+       if (error != NULL)
+               gtk_label_set_label (GTK_LABEL (widget), error->message);
+       gtk_widget_set_visible (widget, FALSE);
+       return TRUE;
+}
+
+static void
+gs_shell_events_rescan (GsShell *shell)
+{
+       GsPluginEvent *event;
+       GsShellPrivate *priv = gs_shell_get_instance_private (shell);
+       GtkWidget *widget;
+       guint i;
+
+       /* find the first active event and show it */
+       event = gs_plugin_loader_get_event_default (priv->plugin_loader);
+       if (event != NULL) {
+               if (!gs_shell_events_show (shell, event)) {
+                       gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID);
+                       return;
+               }
+               gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_VISIBLE);
+               return;
+       }
+
+       /* nothing to show */
+       g_debug ("no events to show");
+       widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "notification_event"));
+       gtk_widget_set_visible (widget, FALSE);
+}
+
+static void
+gs_shell_events_notify_cb (GsPluginLoader *plugin_loader,
+                          GParamSpec *pspec,
+                          GsShell *shell)
+{
+       gs_shell_events_rescan (shell);
+}
+
+static void
+gs_shell_plugin_event_dismissed_cb (GdNotification *notification, GsShell *shell)
+{
+       GPtrArray *events;
+       GsShellPrivate *priv = gs_shell_get_instance_private (shell);
+       guint i;
+
+       /* mark any events currently showing as invalid */
+       events = gs_plugin_loader_get_events (priv->plugin_loader);
+       for (i = 0; i < events->len; i++) {
+               GsPluginEvent *event = g_ptr_array_index (events, i);
+               if (gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_VISIBLE)) {
+                       gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID);
+                       gs_plugin_event_remove_flag (event, GS_PLUGIN_EVENT_FLAG_VISIBLE);
+               }
+       }
+
+       /* show the next event */
+       gs_shell_events_rescan (shell);
+}
+
 void
 gs_shell_setup (GsShell *shell, GsPluginLoader *plugin_loader, GCancellable *cancellable)
 {
@@ -625,6 +822,9 @@ gs_shell_setup (GsShell *shell, GsPluginLoader *plugin_loader, GCancellable *can
        priv->plugin_loader = g_object_ref (plugin_loader);
        g_signal_connect (priv->plugin_loader, "reload",
                          G_CALLBACK (gs_shell_reload_cb), shell);
+       g_signal_connect_object (priv->plugin_loader, "notify::events",
+                                G_CALLBACK (gs_shell_events_notify_cb),
+                                shell, 0);
        priv->cancellable = g_object_ref (cancellable);
 
        gs_shell_monitor_permission (shell);
@@ -683,6 +883,14 @@ gs_shell_setup (GsShell *shell, GsPluginLoader *plugin_loader, GCancellable *can
        g_signal_connect (widget, "clicked",
                          G_CALLBACK (gs_shell_overview_button_cb), shell);
 
+       /* set up infobar */
+       widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "notification_event"));
+       g_signal_connect (widget, "dismissed",
+                         G_CALLBACK (gs_shell_plugin_event_dismissed_cb), shell);
+       widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_sources"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_shell_plugin_events_sources_cb), shell);
+
        priv->shell_overview = GS_SHELL_OVERVIEW (gtk_builder_get_object (priv->builder, "shell_overview"));
        gs_shell_overview_setup (priv->shell_overview,
                                 shell,
diff --git a/src/plugins/gs-plugin-dummy.c b/src/plugins/gs-plugin-dummy.c
index cb353f4..bd0b2c8 100644
--- a/src/plugins/gs-plugin-dummy.c
+++ b/src/plugins/gs-plugin-dummy.c
@@ -371,11 +371,14 @@ gs_plugin_update_app (GsPlugin *plugin,
                       gs_plugin_get_name (plugin)) != 0)
                return TRUE;
 
+       //gs_plugin_error_nonfatal (error)
+
        /* always fail */
        g_set_error_literal (error,
                             GS_PLUGIN_ERROR,
-                            GS_PLUGIN_ERROR_NO_NETWORK,
+                            GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
                             "no network connection is available");
+       gs_plugin_error_add_unique_id (error, app);
        return FALSE;
 }
 



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