summaryrefslogtreecommitdiffstats
path: root/src/gs-app-reviews-dialog.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gs-app-reviews-dialog.c')
-rw-r--r--src/gs-app-reviews-dialog.c559
1 files changed, 559 insertions, 0 deletions
diff --git a/src/gs-app-reviews-dialog.c b/src/gs-app-reviews-dialog.c
new file mode 100644
index 0000000..a00bef0
--- /dev/null
+++ b/src/gs-app-reviews-dialog.c
@@ -0,0 +1,559 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) Adrien Plazas <adrien.plazas@puri.sm>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include "gs-app-reviews-dialog.h"
+
+#include "gnome-software-private.h"
+#include "gs-common.h"
+#include "gs-review-row.h"
+#include <glib/gi18n.h>
+
+struct _GsAppReviewsDialog
+{
+ GtkDialog parent_instance;
+ GtkWidget *listbox;
+ GtkWidget *stack;
+
+ GsPluginLoader *plugin_loader; /* (owned) (nullable) */
+ GsApp *app; /* (owned) (nullable) */
+ GCancellable *cancellable; /* (owned) */
+ GCancellable *refine_cancellable; /* (owned) (nullable) */
+ GsOdrsProvider *odrs_provider; /* (nullable) (owned), NULL if reviews are disabled */
+};
+
+G_DEFINE_TYPE (GsAppReviewsDialog, gs_app_reviews_dialog, GTK_TYPE_DIALOG)
+
+typedef enum {
+ PROP_APP = 1,
+ PROP_ODRS_PROVIDER,
+ PROP_PLUGIN_LOADER,
+} GsAppReviewsDialogProperty;
+
+static GParamSpec *obj_props[PROP_PLUGIN_LOADER + 1] = { NULL, };
+
+enum {
+ SIGNAL_REVIEWS_UPDATED,
+ SIGNAL_LAST
+};
+
+static guint signals[SIGNAL_LAST] = { 0 };
+
+static void refresh_reviews (GsAppReviewsDialog *self);
+
+static gint
+sort_reviews (AsReview **a, AsReview **b)
+{
+ return -g_date_time_compare (as_review_get_date (*a), as_review_get_date (*b));
+}
+
+static void
+review_button_clicked_cb (GsReviewRow *row,
+ GsReviewAction action,
+ GsAppReviewsDialog *self)
+{
+ AsReview *review = gs_review_row_get_review (row);
+ g_autoptr(GError) local_error = NULL;
+
+ g_assert (self->odrs_provider != NULL);
+
+ /* FIXME: Make this async */
+ switch (action) {
+ case GS_REVIEW_ACTION_UPVOTE:
+ gs_odrs_provider_upvote_review (self->odrs_provider, self->app,
+ review, self->cancellable,
+ &local_error);
+ break;
+ case GS_REVIEW_ACTION_DOWNVOTE:
+ gs_odrs_provider_downvote_review (self->odrs_provider, self->app,
+ review, self->cancellable,
+ &local_error);
+ break;
+ case GS_REVIEW_ACTION_REPORT:
+ gs_odrs_provider_report_review (self->odrs_provider, self->app,
+ review, self->cancellable,
+ &local_error);
+ break;
+ case GS_REVIEW_ACTION_REMOVE:
+ gs_odrs_provider_remove_review (self->odrs_provider, self->app,
+ review, self->cancellable,
+ &local_error);
+ break;
+ case GS_REVIEW_ACTION_DISMISS:
+ /* The dismiss action is only used from the moderate page. */
+ default:
+ g_assert_not_reached ();
+ }
+
+ if (local_error != NULL) {
+ g_warning ("failed to set review on %s: %s",
+ gs_app_get_id (self->app), local_error->message);
+ return;
+ }
+
+ refresh_reviews (self);
+}
+
+static void
+populate_reviews (GsAppReviewsDialog *self)
+{
+ GPtrArray *reviews;
+ gboolean show_reviews = FALSE;
+ guint64 possible_actions = 0;
+ guint i;
+ GsReviewAction all_actions[] = {
+ GS_REVIEW_ACTION_UPVOTE,
+ GS_REVIEW_ACTION_DOWNVOTE,
+ GS_REVIEW_ACTION_REPORT,
+ GS_REVIEW_ACTION_REMOVE,
+ };
+
+ /* nothing to show */
+ if (self->app == NULL) {
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "empty");
+
+ return;
+ }
+
+ /* show or hide the entire reviews section */
+ switch (gs_app_get_kind (self->app)) {
+ case AS_COMPONENT_KIND_DESKTOP_APP:
+ case AS_COMPONENT_KIND_FONT:
+ case AS_COMPONENT_KIND_INPUT_METHOD:
+ case AS_COMPONENT_KIND_WEB_APP:
+ /* don't show a missing rating on a local file */
+ if (gs_app_get_state (self->app) != GS_APP_STATE_AVAILABLE_LOCAL &&
+ self->odrs_provider != NULL)
+ show_reviews = TRUE;
+ break;
+ default:
+ break;
+ }
+
+ /* some apps are unreviewable */
+ if (gs_app_has_quirk (self->app, GS_APP_QUIRK_NOT_REVIEWABLE))
+ show_reviews = FALSE;
+
+ /* check that reviews are available */
+ reviews = gs_app_get_reviews (self->app);
+ if (reviews->len == 0)
+ show_reviews = FALSE;
+
+ if (!show_reviews) {
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "empty");
+
+ return;
+ }
+
+ /* find what the plugins support */
+ for (i = 0; i < G_N_ELEMENTS (all_actions); i++) {
+ if (self->odrs_provider != NULL)
+ possible_actions |= (1u << all_actions[i]);
+ }
+
+ /* add all the reviews */
+ gs_widget_remove_all (self->listbox, (GsRemoveFunc) gtk_list_box_remove);
+ g_ptr_array_sort (reviews, (GCompareFunc) sort_reviews);
+ for (i = 0; i < reviews->len; i++) {
+ AsReview *review = g_ptr_array_index (reviews, i);
+ GtkWidget *row = gs_review_row_new (review);
+ guint64 actions;
+
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE);
+ gtk_list_box_append (GTK_LIST_BOX (self->listbox), row);
+
+ g_signal_connect (row, "button-clicked",
+ G_CALLBACK (review_button_clicked_cb), self);
+ if (as_review_get_flags (review) & AS_REVIEW_FLAG_SELF)
+ actions = possible_actions & (1 << GS_REVIEW_ACTION_REMOVE);
+ else
+ actions = possible_actions & ~(1u << GS_REVIEW_ACTION_REMOVE);
+ gs_review_row_set_actions (GS_REVIEW_ROW (row), actions);
+ gs_review_row_set_network_available (GS_REVIEW_ROW (row),
+ GS_IS_PLUGIN_LOADER (self->plugin_loader) && gs_plugin_loader_get_network_available (self->plugin_loader));
+ }
+
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "reviews");
+}
+
+static void
+refresh_reviews (GsAppReviewsDialog *self)
+{
+ if (!gtk_widget_get_realized (GTK_WIDGET (self)))
+ return;
+
+ populate_reviews (self);
+
+ g_signal_emit (self, signals[SIGNAL_REVIEWS_UPDATED], 0);
+}
+
+static void
+gs_app_reviews_dialog_app_refine_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ GsAppReviewsDialog *self = GS_APP_REVIEWS_DIALOG (user_data);
+ g_autoptr(GError) error = NULL;
+
+ if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) {
+ g_warning ("failed to refine %s: %s",
+ gs_app_get_id (self->app),
+ error->message);
+ return;
+ }
+
+ refresh_reviews (self);
+}
+
+static void
+gs_app_reviews_dialog_app_refine (GsAppReviewsDialog *self)
+{
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ if (self->refine_cancellable != NULL) {
+ g_cancellable_cancel (self->refine_cancellable);
+ g_clear_object (&self->refine_cancellable);
+ }
+
+ if (self->plugin_loader == NULL || self->app == NULL)
+ return;
+
+ self->refine_cancellable = g_cancellable_new ();
+
+ /* If this task fails (e.g. because we have no networking) then
+ * it's of no huge importance if we don't get the required data.
+ */
+ plugin_job = gs_plugin_job_refine_new_for_app (self->app,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->refine_cancellable,
+ gs_app_reviews_dialog_app_refine_cb,
+ self);
+}
+
+static void
+gs_app_reviews_dialog_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GsAppReviewsDialog *self = GS_APP_REVIEWS_DIALOG (object);
+
+ switch ((GsAppReviewsDialogProperty) prop_id) {
+ case PROP_APP:
+ g_value_set_object (value, gs_app_reviews_dialog_get_app (self));
+ break;
+ case PROP_ODRS_PROVIDER:
+ g_value_set_object (value, gs_app_reviews_dialog_get_odrs_provider (self));
+ break;
+ case PROP_PLUGIN_LOADER:
+ g_value_set_object (value, gs_app_reviews_dialog_get_plugin_loader (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_reviews_dialog_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GsAppReviewsDialog *self = GS_APP_REVIEWS_DIALOG (object);
+
+ switch ((GsAppReviewsDialogProperty) prop_id) {
+ case PROP_APP:
+ gs_app_reviews_dialog_set_app (self, g_value_get_object (value));
+ break;
+ case PROP_ODRS_PROVIDER:
+ gs_app_reviews_dialog_set_odrs_provider (self, g_value_get_object (value));
+ break;
+ case PROP_PLUGIN_LOADER:
+ gs_app_reviews_dialog_set_plugin_loader (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_reviews_dialog_dispose (GObject *object)
+{
+ GsAppReviewsDialog *self = GS_APP_REVIEWS_DIALOG (object);
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+
+ if (self->refine_cancellable != NULL) {
+ g_cancellable_cancel (self->refine_cancellable);
+ g_clear_object (&self->refine_cancellable);
+ }
+
+ if (self->plugin_loader)
+ g_signal_handlers_disconnect_by_func (self->plugin_loader,
+ refresh_reviews, self);
+ g_clear_object (&self->plugin_loader);
+
+ g_clear_object (&self->app);
+ g_clear_object (&self->odrs_provider);
+
+ G_OBJECT_CLASS (gs_app_reviews_dialog_parent_class)->dispose (object);
+}
+
+static void
+gs_app_reviews_dialog_class_init (GsAppReviewsDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gs_app_reviews_dialog_get_property;
+ object_class->set_property = gs_app_reviews_dialog_set_property;
+ object_class->dispose = gs_app_reviews_dialog_dispose;
+
+ /**
+ * GsAppReviewsDialog:app: (nullable)
+ *
+ * An app whose reviews should be displayed.
+ *
+ * If this is %NULL, ratings and reviews will be disabled.
+ *
+ * Since: 42
+ */
+ obj_props[PROP_APP] =
+ g_param_spec_object ("app", NULL, NULL,
+ GS_TYPE_APP,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GsAppReviewsDialog:odrs-provider: (nullable)
+ *
+ * An ODRS provider to give access to ratings and reviews information
+ * for the app being displayed.
+ *
+ * If this is %NULL, ratings and reviews will be disabled.
+ *
+ * Since: 42
+ */
+ obj_props[PROP_ODRS_PROVIDER] =
+ g_param_spec_object ("odrs-provider", NULL, NULL,
+ GS_TYPE_ODRS_PROVIDER,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GsAppReviewsDialog:plugin-loader: (nullable)
+ *
+ * A plugin loader to provide network availability.
+ *
+ * If this is %NULL, ratings and reviews will be disabled.
+ *
+ * Since: 42
+ */
+ obj_props[PROP_PLUGIN_LOADER] =
+ g_param_spec_object ("plugin-loader", NULL, NULL,
+ GS_TYPE_PLUGIN_LOADER,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props);
+
+ /**
+ * GsAppReviewsDialog::reviews-updated:
+ *
+ * Emitted when reviews are updated.
+ *
+ * Since: 42
+ */
+ signals[SIGNAL_REVIEWS_UPDATED] =
+ g_signal_new ("reviews-updated",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-app-reviews-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewsDialog, listbox);
+ gtk_widget_class_bind_template_child (widget_class, GsAppReviewsDialog, stack);
+}
+
+static void
+gs_app_reviews_dialog_init (GsAppReviewsDialog *self)
+{
+ self->cancellable = g_cancellable_new ();
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ g_signal_connect_swapped (self, "realize",
+ G_CALLBACK (refresh_reviews), self);
+}
+
+/**
+ * gs_app_reviews_dialog_new:
+ * @parent: (nullable): a #GtkWindow, or %NULL
+ * @app: (nullable): a #GsApp, or %NULL
+ * @odrs_provider: (nullable): a #GsOdrsProvider, or %NULL
+ * @plugin_loader: (nullable): a #GsPluginLoader, or %NULL
+ *
+ * Create a new #GsAppReviewsDialog transient for @parent, and set its initial
+ * app, ODRS provider and plugin loader to @app, @odrs_provider and
+ * @plugin_loader respectively.
+ *
+ * Returns: (transfer full): a new #GsAppReviewsDialog
+ * Since: 42
+ */
+GtkWidget *
+gs_app_reviews_dialog_new (GtkWindow *parent, GsApp *app, GsOdrsProvider *odrs_provider, GsPluginLoader *plugin_loader)
+{
+ GsAppReviewsDialog *self;
+
+ g_return_val_if_fail (parent == NULL || GTK_IS_WINDOW (parent), NULL);
+ g_return_val_if_fail (app == NULL || GS_IS_APP (app), NULL);
+ g_return_val_if_fail (odrs_provider == NULL || GS_IS_ODRS_PROVIDER (odrs_provider), NULL);
+ g_return_val_if_fail (plugin_loader == NULL ||GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+
+ self = g_object_new (GS_TYPE_APP_REVIEWS_DIALOG,
+ "app", app,
+ "modal", TRUE,
+ "odrs-provider", odrs_provider,
+ "plugin-loader", plugin_loader,
+ "transient-for", parent,
+ "use-header-bar", TRUE,
+ NULL);
+
+ return GTK_WIDGET (self);
+}
+
+/**
+ * gs_app_reviews_dialog_get_app:
+ * @self: a #GsAppReviewsDialog
+ *
+ * Get the value of #GsAppReviewsDialog:app.
+ *
+ * Returns: (nullable) (transfer none): a #GsApp, or %NULL if unset
+ * Since: 42
+ */
+GsApp *
+gs_app_reviews_dialog_get_app (GsAppReviewsDialog *self)
+{
+ g_return_val_if_fail (GS_IS_APP_REVIEWS_DIALOG (self), NULL);
+
+ return self->app;
+}
+
+/**
+ * gs_app_reviews_dialog_set_app:
+ * @self: a #GsAppReviewsDialog
+ * @app: (nullable) (transfer none): new #GsApp or %NULL
+ *
+ * Set the value of #GsAppReviewsDialog:app.
+ *
+ * Since: 42
+ */
+void
+gs_app_reviews_dialog_set_app (GsAppReviewsDialog *self,
+ GsApp *app)
+{
+ g_return_if_fail (GS_IS_APP_REVIEWS_DIALOG (self));
+ g_return_if_fail (app == NULL || GS_IS_APP (app));
+
+ if (g_set_object (&self->app, app)) {
+ gs_app_reviews_dialog_app_refine (self);
+ refresh_reviews (self);
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_APP]);
+ }
+}
+
+/**
+ * gs_app_reviews_dialog_get_odrs_provider:
+ * @self: a #GsAppReviewsDialog
+ *
+ * Get the value of #GsAppReviewsDialog:odrs-provider.
+ *
+ * Returns: (nullable) (transfer none): a #GsOdrsProvider, or %NULL if unset
+ * Since: 42
+ */
+GsOdrsProvider *
+gs_app_reviews_dialog_get_odrs_provider (GsAppReviewsDialog *self)
+{
+ g_return_val_if_fail (GS_IS_APP_REVIEWS_DIALOG (self), NULL);
+
+ return self->odrs_provider;
+}
+
+/**
+ * gs_app_reviews_dialog_set_odrs_provider:
+ * @self: a #GsAppReviewsDialog
+ * @odrs_provider: (nullable) (transfer none): new #GsOdrsProvider or %NULL
+ *
+ * Set the value of #GsAppReviewsDialog:odrs-provider.
+ *
+ * Since: 42
+ */
+void
+gs_app_reviews_dialog_set_odrs_provider (GsAppReviewsDialog *self,
+ GsOdrsProvider *odrs_provider)
+{
+ g_return_if_fail (GS_IS_APP_REVIEWS_DIALOG (self));
+ g_return_if_fail (odrs_provider == NULL || GS_IS_ODRS_PROVIDER (odrs_provider));
+
+ if (g_set_object (&self->odrs_provider, odrs_provider)) {
+ refresh_reviews (self);
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_ODRS_PROVIDER]);
+ }
+}
+
+/**
+ * gs_app_reviews_dialog_get_plugin_loader:
+ * @self: a #GsAppReviewsDialog
+ *
+ * Get the value of #GsAppReviewsDialog:plugin-loader.
+ *
+ * Returns: (nullable) (transfer none): a #GsPluginLoader, or %NULL if unset
+ * Since: 42
+ */
+GsPluginLoader *
+gs_app_reviews_dialog_get_plugin_loader (GsAppReviewsDialog *self)
+{
+ g_return_val_if_fail (GS_IS_APP_REVIEWS_DIALOG (self), NULL);
+
+ return self->plugin_loader;
+}
+
+/**
+ * gs_app_reviews_dialog_set_plugin_loader:
+ * @self: a #GsAppReviewsDialog
+ * @plugin_loader: (nullable) (transfer none): new #GsPluginLoader or %NULL
+ *
+ * Set the value of #GsAppReviewsDialog:plugin-loader.
+ *
+ * Since: 42
+ */
+void
+gs_app_reviews_dialog_set_plugin_loader (GsAppReviewsDialog *self,
+ GsPluginLoader *plugin_loader)
+{
+ g_return_if_fail (GS_IS_APP_REVIEWS_DIALOG (self));
+ g_return_if_fail (plugin_loader == NULL || GS_IS_PLUGIN_LOADER (plugin_loader));
+
+ if (self->plugin_loader)
+ g_signal_handlers_disconnect_by_func (self->plugin_loader,
+ refresh_reviews, self);
+
+ if (g_set_object (&self->plugin_loader, plugin_loader)) {
+ gs_app_reviews_dialog_app_refine (self);
+ g_signal_connect_swapped (self->plugin_loader, "notify::network-available",
+ G_CALLBACK (refresh_reviews), self);
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PLUGIN_LOADER]);
+ }
+}