From 6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:57:27 +0200 Subject: Adding upstream version 43.5. Signed-off-by: Daniel Baumann --- src/gs-app-reviews-dialog.c | 559 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 559 insertions(+) create mode 100644 src/gs-app-reviews-dialog.c (limited to 'src/gs-app-reviews-dialog.c') 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 + * + * 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 + +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]); + } +} -- cgit v1.2.3