summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJamie Murphy <hello@itsjamie.dev>2023-01-16 19:55:35 -0800
committerMarge Bot <marge-bot@gnome.org>2023-02-11 18:16:33 +0000
commit19499b9a3fd6c8b0c32600309456bc35dbc29f52 (patch)
treea0a55c8868e2ab7ceee82d13c4fa0b5f1268074f /src
parente3e18dc1d518e26e8a746dc34162682e6addc78f (diff)
downloadepiphany-19499b9a3fd6c8b0c32600309456bc35dbc29f52.tar.gz
Use popovers for passwords and permissions
Part-of: <https://gitlab.gnome.org/GNOME/epiphany/-/merge_requests/1265>
Diffstat (limited to 'src')
-rw-r--r--src/ephy-location-entry.c139
-rw-r--r--src/ephy-location-entry.h14
-rw-r--r--src/ephy-password-popover.c206
-rw-r--r--src/ephy-password-popover.h36
-rw-r--r--src/ephy-permission-popover.c310
-rw-r--r--src/ephy-permission-popover.h49
-rw-r--r--src/ephy-window.c331
-rw-r--r--src/meson.build2
-rw-r--r--src/resources/ephy-permission-camera-symbolic.svg4
-rw-r--r--src/resources/ephy-permission-generic-symbolic.svg8
-rw-r--r--src/resources/ephy-permission-location-symbolic.svg4
-rw-r--r--src/resources/ephy-permission-microphone-symbolic.svg4
-rw-r--r--src/resources/ephy-permission-notifications-symbolic.svg4
-rw-r--r--src/resources/epiphany.gresource.xml7
-rw-r--r--src/resources/gtk/location-entry.ui12
-rw-r--r--src/resources/gtk/password-popover.ui101
-rw-r--r--src/resources/gtk/permission-popover.ui88
-rw-r--r--src/resources/style-hc.css6
-rw-r--r--src/resources/style.css33
19 files changed, 1357 insertions, 1 deletions
diff --git a/src/ephy-location-entry.c b/src/ephy-location-entry.c
index 402034cf2..1cb278899 100644
--- a/src/ephy-location-entry.c
+++ b/src/ephy-location-entry.c
@@ -57,9 +57,11 @@ struct _EphyLocationEntry {
GtkWidget *text;
GtkWidget *progress;
GtkWidget *security_button;
+ GtkWidget *password_button;
GtkWidget *bookmark_button;
GtkWidget *reader_mode_button;
GList *page_actions;
+ GList *permission_buttons;
GtkWidget *suggestions_popover;
GtkWidget *scrolled_window;
@@ -867,6 +869,15 @@ root_notify_is_active_cb (EphyLocationEntry *entry)
}
static void
+on_permission_popover_response (EphyPermissionPopover *popover,
+ GtkMenuButton *button)
+{
+ EphyLocationEntry *entry = EPHY_LOCATION_ENTRY (gtk_widget_get_parent (GTK_WIDGET (button)));
+ gtk_widget_unparent (GTK_WIDGET (button));
+ entry->permission_buttons = g_list_remove (entry->permission_buttons, button);
+}
+
+static void
update_reader_icon (EphyLocationEntry *entry)
{
GdkDisplay *display;
@@ -981,6 +992,15 @@ ephy_location_entry_measure (GtkWidget *widget,
gtk_widget_measure (entry->text, orientation, for_size, &min, &nat, NULL, NULL);
/* Any other icons need to be similarly added to the text width */
+ for (l = entry->permission_buttons; l; l = l->next) {
+ if (gtk_widget_should_layout (GTK_WIDGET (l->data))) {
+ gtk_widget_measure (GTK_WIDGET (l->data), orientation, for_size,
+ &child_min, &child_nat, NULL, NULL);
+ min += child_min;
+ nat += child_nat;
+ }
+ }
+
if (gtk_widget_should_layout (entry->security_button)) {
gtk_widget_measure (entry->security_button, orientation, for_size,
&child_min, &child_nat, NULL, NULL);
@@ -988,6 +1008,13 @@ ephy_location_entry_measure (GtkWidget *widget,
nat += child_nat;
}
+ if (gtk_widget_should_layout (entry->password_button)) {
+ gtk_widget_measure (entry->password_button, orientation, for_size,
+ &child_min, &child_nat, NULL, NULL);
+ min += child_min;
+ nat += child_nat;
+ }
+
if (gtk_widget_should_layout (entry->bookmark_button)) {
gtk_widget_measure (entry->bookmark_button, orientation, for_size,
&child_min, &child_nat, NULL, NULL);
@@ -1070,8 +1097,14 @@ ephy_location_entry_size_allocate (GtkWidget *widget,
GskTransform *transform;
GList *l;
+ for (l = entry->permission_buttons; l; l = l->next) {
+ allocate_icon (widget, height, baseline, l->data,
+ GTK_PACK_START, &icon_left_pos, &icon_right_pos);
+ }
allocate_icon (widget, height, baseline, entry->security_button,
GTK_PACK_START, &icon_left_pos, &icon_right_pos);
+ allocate_icon (widget, height, baseline, entry->password_button,
+ GTK_PACK_END, &icon_left_pos, &icon_right_pos);
allocate_icon (widget, height, baseline, entry->bookmark_button,
GTK_PACK_END, &icon_left_pos, &icon_right_pos);
allocate_icon (widget, height, baseline, entry->reader_mode_button,
@@ -1227,6 +1260,7 @@ static void
ephy_location_entry_dispose (GObject *object)
{
EphyLocationEntry *entry = EPHY_LOCATION_ENTRY (object);
+ GList *l;
g_clear_handle_id (&entry->progress_timeout, g_source_remove);
@@ -1235,10 +1269,14 @@ ephy_location_entry_dispose (GObject *object)
ephy_location_entry_page_action_clear (entry);
+ for (l = entry->permission_buttons; l; l = l->next)
+ gtk_widget_unparent (GTK_WIDGET (l->data));
+
gtk_widget_unparent (entry->context_menu);
gtk_widget_unparent (entry->text);
gtk_widget_unparent (entry->progress);
gtk_widget_unparent (entry->security_button);
+ gtk_widget_unparent (entry->password_button);
gtk_widget_unparent (entry->bookmark_button);
gtk_widget_unparent (entry->reader_mode_button);
gtk_widget_unparent (entry->suggestions_popover);
@@ -1394,6 +1432,7 @@ ephy_location_entry_class_init (EphyLocationEntryClass *klass)
gtk_widget_class_bind_template_child (widget_class, EphyLocationEntry, text);
gtk_widget_class_bind_template_child (widget_class, EphyLocationEntry, progress);
gtk_widget_class_bind_template_child (widget_class, EphyLocationEntry, security_button);
+ gtk_widget_class_bind_template_child (widget_class, EphyLocationEntry, password_button);
gtk_widget_class_bind_template_child (widget_class, EphyLocationEntry, bookmark_button);
gtk_widget_class_bind_template_child (widget_class, EphyLocationEntry, reader_mode_button);
gtk_widget_class_bind_template_child (widget_class, EphyLocationEntry, suggestions_popover);
@@ -1773,6 +1812,106 @@ ephy_location_entry_set_lock_tooltip (EphyLocationEntry *entry,
}
void
+ephy_location_entry_add_permission_popover (EphyLocationEntry *entry,
+ EphyPermissionPopover *popover)
+{
+ GtkMenuButton *menu_button;
+
+ g_assert (EPHY_IS_LOCATION_ENTRY (entry));
+ g_assert (EPHY_IS_PERMISSION_POPOVER (popover));
+
+ menu_button = GTK_MENU_BUTTON (gtk_menu_button_new ());
+
+ switch (ephy_permission_popover_get_permission_type (popover)) {
+ case EPHY_PERMISSION_TYPE_SHOW_NOTIFICATIONS:
+ gtk_menu_button_set_icon_name (menu_button, "ephy-permission-notifications-symbolic");
+ gtk_widget_set_tooltip_text (GTK_WIDGET (menu_button), _("Notification Request"));
+ break;
+ case EPHY_PERMISSION_TYPE_ACCESS_WEBCAM:
+ gtk_menu_button_set_icon_name (menu_button, "ephy-permission-camera-symbolic");
+ gtk_widget_set_tooltip_text (GTK_WIDGET (menu_button), _("Camera Request"));
+ break;
+ case EPHY_PERMISSION_TYPE_ACCESS_MICROPHONE:
+ gtk_menu_button_set_icon_name (menu_button, "ephy-permission-microphone-symbolic");
+ gtk_widget_set_tooltip_text (GTK_WIDGET (menu_button), _("Microphone Request"));
+ break;
+ case EPHY_PERMISSION_TYPE_ACCESS_LOCATION:
+ gtk_menu_button_set_icon_name (menu_button, "ephy-permission-location-symbolic");
+ gtk_widget_set_tooltip_text (GTK_WIDGET (menu_button), _("Location Request"));
+ break;
+ case EPHY_PERMISSION_TYPE_ACCESS_WEBCAM_AND_MICROPHONE:
+ gtk_menu_button_set_icon_name (menu_button, "ephy-permission-generic-symbolic");
+ gtk_widget_set_tooltip_text (GTK_WIDGET (menu_button), _("Webcam and Microphone Request"));
+ break;
+ default:
+ gtk_menu_button_set_icon_name (menu_button, "ephy-permission-generic-symbolic");
+ gtk_widget_set_tooltip_text (GTK_WIDGET (menu_button), _("Permission Request"));
+ }
+
+ gtk_widget_set_valign (GTK_WIDGET (menu_button), GTK_ALIGN_CENTER);
+ gtk_menu_button_set_popover (menu_button, GTK_WIDGET (popover));
+
+ gtk_widget_add_css_class (GTK_WIDGET (menu_button), "entry-icon");
+ gtk_widget_add_css_class (GTK_WIDGET (menu_button), "start");
+
+ gtk_widget_set_parent (GTK_WIDGET (menu_button), GTK_WIDGET (entry));
+ entry->permission_buttons = g_list_prepend (entry->permission_buttons, menu_button);
+ g_signal_connect (popover, "allow", G_CALLBACK (on_permission_popover_response), menu_button);
+ g_signal_connect (popover, "deny", G_CALLBACK (on_permission_popover_response), menu_button);
+}
+
+void
+ephy_location_entry_show_best_permission_popover (EphyLocationEntry *entry)
+{
+ g_assert (EPHY_IS_LOCATION_ENTRY (entry));
+
+ if (entry->permission_buttons) {
+ GtkWidget *menu_button = g_list_last (entry->permission_buttons)->data;
+
+ gtk_menu_button_popup (GTK_MENU_BUTTON (menu_button));
+ }
+}
+
+void
+ephy_location_entry_clear_permission_buttons (EphyLocationEntry *entry)
+{
+ GList *l;
+
+ g_assert (EPHY_IS_LOCATION_ENTRY (entry));
+
+ for (l = entry->permission_buttons; l; l = l->next) {
+ GtkMenuButton *button = l->data;
+ GtkPopover *popover = gtk_menu_button_get_popover (button);
+
+ g_signal_handlers_disconnect_by_func (popover, G_CALLBACK (on_permission_popover_response), button);
+
+ gtk_widget_unparent (GTK_WIDGET (button));
+ }
+
+ g_clear_pointer (&entry->permission_buttons, g_list_free);
+}
+
+void
+ephy_location_entry_set_password_popover (EphyLocationEntry *entry,
+ EphyPasswordPopover *popover)
+{
+ g_assert (EPHY_IS_LOCATION_ENTRY (entry));
+ g_assert (popover == NULL || EPHY_IS_PASSWORD_POPOVER (popover));
+
+ gtk_menu_button_set_popover (GTK_MENU_BUTTON (entry->password_button),
+ GTK_WIDGET (popover));
+ gtk_widget_set_visible (entry->password_button, popover != NULL);
+}
+
+void
+ephy_location_entry_show_password_popover (EphyLocationEntry *entry)
+{
+ g_assert (EPHY_IS_LOCATION_ENTRY (entry));
+
+ gtk_menu_button_popup (GTK_MENU_BUTTON (entry->password_button));
+}
+
+void
ephy_location_entry_set_add_bookmark_popover (EphyLocationEntry *entry,
GtkPopover *popover)
{
diff --git a/src/ephy-location-entry.h b/src/ephy-location-entry.h
index 40ce38925..0071569f0 100644
--- a/src/ephy-location-entry.h
+++ b/src/ephy-location-entry.h
@@ -27,6 +27,8 @@
#include "ephy-adaptive-mode.h"
#include "ephy-bookmark-states.h"
+#include "ephy-password-popover.h"
+#include "ephy-permission-popover.h"
#include "ephy-security-levels.h"
G_BEGIN_DECLS
@@ -54,6 +56,18 @@ void ephy_location_entry_set_bookmark_icon_state (EphyLocationEntr
void ephy_location_entry_set_lock_tooltip (EphyLocationEntry *entry,
const char *tooltip);
+void ephy_location_entry_set_password_popover (EphyLocationEntry *entry,
+ EphyPasswordPopover *popover);
+
+void ephy_location_entry_show_password_popover (EphyLocationEntry *entry);
+
+void ephy_location_entry_add_permission_popover (EphyLocationEntry *entry,
+ EphyPermissionPopover *popover);
+
+void ephy_location_entry_show_best_permission_popover (EphyLocationEntry *entry);
+
+void ephy_location_entry_clear_permission_buttons (EphyLocationEntry *entry);
+
void ephy_location_entry_set_add_bookmark_popover (EphyLocationEntry *entry,
GtkPopover *popover);
diff --git a/src/ephy-password-popover.c b/src/ephy-password-popover.c
new file mode 100644
index 000000000..acd79a43a
--- /dev/null
+++ b/src/ephy-password-popover.c
@@ -0,0 +1,206 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2023 Jamie Murphy <hello@itsjamie.dev>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ephy-password-popover.h"
+
+#include "ephy-window.h"
+
+struct _EphyPasswordPopover {
+ GtkPopover parent_instance;
+
+ GtkWidget *username_entry;
+ GtkWidget *password_entry;
+
+ EphyPasswordRequestData *request_data;
+};
+
+G_DEFINE_FINAL_TYPE (EphyPasswordPopover, ephy_password_popover, GTK_TYPE_POPOVER)
+
+enum {
+ PROP_0,
+ PROP_REQUEST_DATA,
+ LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+enum {
+ RESPONSE,
+ LAST_SIGNAL
+};
+
+static gint signals[LAST_SIGNAL] = { 0 };
+
+static void
+on_entry_changed (EphyPasswordPopover *self,
+ GtkEditable *entry)
+{
+ const char *text = gtk_editable_get_text (entry);
+
+ if (entry == GTK_EDITABLE (self->username_entry))
+ self->request_data->username = g_strdup (text);
+
+ if (entry == GTK_EDITABLE (self->password_entry))
+ self->request_data->password = g_strdup (text);
+}
+
+static void
+on_password_never (EphyPasswordPopover *self,
+ GtkButton *button)
+{
+ EphyEmbedShell *shell = EPHY_EMBED_SHELL (ephy_embed_shell_get_default ());
+ EphyPermissionsManager *permissions_manager = ephy_embed_shell_get_permissions_manager (shell);
+
+ ephy_permissions_manager_set_permission (permissions_manager,
+ EPHY_PERMISSION_TYPE_SAVE_PASSWORD,
+ self->request_data->origin,
+ EPHY_PERMISSION_DENY);
+
+ gtk_popover_popdown (GTK_POPOVER (self));
+ g_signal_emit (self, signals[RESPONSE], 0);
+}
+
+static void
+on_password_save (EphyPasswordPopover *self,
+ GtkButton *button)
+{
+ EphyEmbedShell *shell = EPHY_EMBED_SHELL (ephy_embed_shell_get_default ());
+ EphyPasswordManager *password_manager = ephy_embed_shell_get_password_manager (shell);
+
+ ephy_password_manager_save (password_manager, self->request_data->origin,
+ self->request_data->target_origin, self->request_data->username,
+ self->request_data->password, self->request_data->usernameField,
+ self->request_data->passwordField, self->request_data->isNew);
+
+ gtk_popover_popdown (GTK_POPOVER (self));
+ g_signal_emit (self, signals[RESPONSE], 0);
+}
+
+static void
+ephy_password_popover_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ EphyPasswordPopover *self = EPHY_PASSWORD_POPOVER (object);
+
+ switch (prop_id) {
+ case PROP_REQUEST_DATA:
+ g_value_set_pointer (value, self->request_data);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_password_popover_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EphyPasswordPopover *self = EPHY_PASSWORD_POPOVER (object);
+
+ switch (prop_id) {
+ case PROP_REQUEST_DATA:
+ self->request_data = g_value_get_pointer (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_password_popover_constructed (GObject *object)
+{
+ EphyPasswordPopover *self = EPHY_PASSWORD_POPOVER (object);
+
+ G_OBJECT_CLASS (ephy_password_popover_parent_class)->constructed (object);
+
+ gtk_editable_set_text (GTK_EDITABLE (self->username_entry),
+ self->request_data->username);
+ gtk_editable_set_text (GTK_EDITABLE (self->password_entry),
+ self->request_data->password);
+}
+
+static void
+ephy_password_popover_finalize (GObject *object)
+{
+ EphyPasswordPopover *popover = EPHY_PASSWORD_POPOVER (object);
+
+ ephy_password_request_data_free (popover->request_data);
+
+ G_OBJECT_CLASS (ephy_password_popover_parent_class)->finalize (object);
+}
+
+static void
+ephy_password_popover_class_init (EphyPasswordPopoverClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = ephy_password_popover_get_property;
+ object_class->set_property = ephy_password_popover_set_property;
+ object_class->constructed = ephy_password_popover_constructed;
+ object_class->finalize = ephy_password_popover_finalize;
+
+ obj_properties[PROP_REQUEST_DATA] =
+ g_param_spec_pointer ("request-data", "", "",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+
+ /**
+ * EphyPasswordPopover::response:
+ * @popover: the object on which the signal is emitted
+ *
+ * Emitted when the user responses to the popover prompt
+ *
+ */
+ signals[RESPONSE] = g_signal_new ("response", G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_FIRST | G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/epiphany/gtk/password-popover.ui");
+ gtk_widget_class_bind_template_child (widget_class, EphyPasswordPopover, username_entry);
+ gtk_widget_class_bind_template_child (widget_class, EphyPasswordPopover, password_entry);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_entry_changed);
+ gtk_widget_class_bind_template_callback (widget_class, on_password_save);
+ gtk_widget_class_bind_template_callback (widget_class, on_password_never);
+}
+
+static void
+ephy_password_popover_init (EphyPasswordPopover *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+EphyPasswordPopover *
+ephy_password_popover_new (EphyPasswordRequestData *request_data)
+{
+ return g_object_new (EPHY_TYPE_PASSWORD_POPOVER,
+ "request-data", request_data,
+ NULL);
+}
diff --git a/src/ephy-password-popover.h b/src/ephy-password-popover.h
new file mode 100644
index 000000000..de30dd99d
--- /dev/null
+++ b/src/ephy-password-popover.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2023 Jamie Murphy <hello@itsjamie.dev>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+#include "ephy-password-manager.h"
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_PASSWORD_POPOVER (ephy_password_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (EphyPasswordPopover, ephy_password_popover, EPHY, PASSWORD_POPOVER, GtkPopover)
+
+EphyPasswordPopover *ephy_password_popover_new (EphyPasswordRequestData *request_data);
+
+G_END_DECLS
diff --git a/src/ephy-permission-popover.c b/src/ephy-permission-popover.c
new file mode 100644
index 000000000..a6e3f3ba9
--- /dev/null
+++ b/src/ephy-permission-popover.c
@@ -0,0 +1,310 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2023 Jamie Murphy <hello@itsjamie.dev>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ephy-embed-shell.h"
+#include "ephy-lib-type-builtins.h"
+#include "ephy-permission-popover.h"
+
+#include <glib/gi18n.h>
+
+struct _EphyPermissionPopover {
+ GtkPopover parent_instance;
+
+ GtkLabel *permission_title;
+ GtkLabel *permission_description;
+
+ EphyPermissionType permission_type;
+ WebKitPermissionRequest *permission_request;
+ char *origin;
+};
+
+G_DEFINE_FINAL_TYPE (EphyPermissionPopover, ephy_permission_popover, GTK_TYPE_POPOVER)
+
+enum {
+ PROP_0,
+ PROP_PERMISSION_TYPE,
+ PROP_PERMISSION_REQUEST,
+ PROP_ORIGIN,
+ LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+enum {
+ ALLOW,
+ DENY,
+ LAST_SIGNAL
+};
+
+static gint signals[LAST_SIGNAL] = { 0 };
+
+static void
+on_permission_deny (EphyPermissionPopover *self)
+{
+ gtk_popover_popdown (GTK_POPOVER (self));
+ g_signal_emit (self, signals[DENY], 0);
+}
+
+static void
+on_permission_allow (EphyPermissionPopover *self)
+{
+ gtk_popover_popdown (GTK_POPOVER (self));
+ g_signal_emit (self, signals[ALLOW], 0);
+}
+
+static void
+ephy_permission_popover_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ EphyPermissionPopover *self = EPHY_PERMISSION_POPOVER (object);
+
+ switch (prop_id) {
+ case PROP_PERMISSION_TYPE:
+ g_value_set_enum (value, self->permission_type);
+ break;
+ case PROP_PERMISSION_REQUEST:
+ g_value_set_object (value, self->permission_request);
+ break;
+ case PROP_ORIGIN:
+ g_value_set_string (value, self->origin);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_permission_popover_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EphyPermissionPopover *self = EPHY_PERMISSION_POPOVER (object);
+
+ switch (prop_id) {
+ case PROP_PERMISSION_TYPE:
+ self->permission_type = g_value_get_enum (value);
+ break;
+ case PROP_PERMISSION_REQUEST:
+ self->permission_request = g_object_ref (g_value_get_object (value));
+ break;
+ case PROP_ORIGIN:
+ self->origin = g_value_dup_string (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_permission_popover_constructed (GObject *object)
+{
+ EphyPermissionPopover *self = EPHY_PERMISSION_POPOVER (object);
+ g_autofree char *title = NULL;
+ g_autofree char *message = NULL;
+
+ ephy_permission_popover_get_text (self, &title, &message);
+
+ gtk_label_set_label (self->permission_title, title);
+ gtk_label_set_label (self->permission_description, message);
+}
+
+static void
+ephy_permission_popover_dispose (GObject *object)
+{
+ EphyPermissionPopover *self = EPHY_PERMISSION_POPOVER (object);
+
+ g_clear_object (&self->permission_request);
+
+ G_OBJECT_CLASS (ephy_permission_popover_parent_class)->dispose (object);
+}
+
+static void
+ephy_permission_popover_finalize (GObject *object)
+{
+ EphyPermissionPopover *self = EPHY_PERMISSION_POPOVER (object);
+
+ g_free (self->origin);
+
+ G_OBJECT_CLASS (ephy_permission_popover_parent_class)->finalize (object);
+}
+
+static void
+ephy_permission_popover_class_init (EphyPermissionPopoverClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = ephy_permission_popover_get_property;
+ object_class->set_property = ephy_permission_popover_set_property;
+ object_class->constructed = ephy_permission_popover_constructed;
+ object_class->dispose = ephy_permission_popover_dispose;
+ object_class->finalize = ephy_permission_popover_finalize;
+
+ obj_properties[PROP_PERMISSION_TYPE] =
+ g_param_spec_enum ("permission-type", "", "", EPHY_TYPE_PERMISSION_TYPE,
+ EPHY_PERMISSION_TYPE_SHOW_NOTIFICATIONS,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_PERMISSION_REQUEST] =
+ g_param_spec_object ("permission-request", "", "", WEBKIT_TYPE_PERMISSION_REQUEST,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_ORIGIN] =
+ g_param_spec_string ("origin", "", "", "",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+
+ /**
+ * EphyPermissionPopover::allow:
+ * @popover: the object on which the signal is emitted
+ *
+ * Emitted when the user presses "Allow" on the popover prompt
+ *
+ */
+ signals[ALLOW] = g_signal_new ("allow", G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_FIRST | G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * EphyPermissionPopover::deny:
+ * @popover: the object on which the signal is emitted
+ *
+ * Emitted when the user presses "Deny" on the popover prompt
+ *
+ */
+ signals[DENY] = g_signal_new ("deny", G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_FIRST | G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/epiphany/gtk/permission-popover.ui");
+ gtk_widget_class_bind_template_child (widget_class, EphyPermissionPopover, permission_title);
+ gtk_widget_class_bind_template_child (widget_class, EphyPermissionPopover, permission_description);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_permission_deny);
+ gtk_widget_class_bind_template_callback (widget_class, on_permission_allow);
+}
+
+static void
+ephy_permission_popover_init (EphyPermissionPopover *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+EphyPermissionPopover *
+ephy_permission_popover_new (EphyPermissionType permission_type,
+ WebKitPermissionRequest *permission_request,
+ const char *origin)
+{
+ return g_object_new (EPHY_TYPE_PERMISSION_POPOVER,
+ "permission-type", permission_type,
+ "permission-request", permission_request,
+ "origin", origin,
+ NULL);
+}
+
+EphyPermissionType
+ephy_permission_popover_get_permission_type (EphyPermissionPopover *self)
+{
+ g_assert (EPHY_IS_PERMISSION_POPOVER (self));
+
+ return self->permission_type;
+}
+
+WebKitPermissionRequest *
+ephy_permission_popover_get_permission_request (EphyPermissionPopover *self)
+{
+ g_assert (EPHY_IS_PERMISSION_POPOVER (self));
+
+ return self->permission_request;
+}
+
+const char *
+ephy_permission_popover_get_origin (EphyPermissionPopover *self)
+{
+ g_assert (EPHY_IS_PERMISSION_POPOVER (self));
+
+ return g_strdup (self->origin);
+}
+
+void
+ephy_permission_popover_get_text (EphyPermissionPopover *self,
+ char **title,
+ char **message)
+{
+ const char *requesting_domain = NULL;
+ const char *current_domain = NULL;
+ g_autofree char *bold_origin = NULL;
+
+ g_assert (EPHY_IS_PERMISSION_POPOVER (self));
+
+ bold_origin = g_markup_printf_escaped ("<b>%s</b>", self->origin);
+ switch (self->permission_type) {
+ case EPHY_PERMISSION_TYPE_SHOW_NOTIFICATIONS:
+ /* Translators: Notification policy for a specific site. */
+ *title = g_strdup (_("Notification Request"));
+ *message = g_strdup_printf (_("The page at “%s” would like to send you notifications"),
+ bold_origin);
+ break;
+ case EPHY_PERMISSION_TYPE_ACCESS_LOCATION:
+ /* Translators: Geolocation policy for a specific site. */
+ *title = g_strdup (_("Location Access Request"));
+ *message = g_strdup_printf (_("The page at “%s” would like to know your location"),
+ bold_origin);
+ break;
+ case EPHY_PERMISSION_TYPE_ACCESS_MICROPHONE:
+ /* Translators: Microphone policy for a specific site. */
+ *title = g_strdup (_("Microphone Access Request"));
+ *message = g_strdup_printf (_("The page at “%s” would like to use your microphone"),
+ bold_origin);
+ break;
+ case EPHY_PERMISSION_TYPE_ACCESS_WEBCAM:
+ /* Translators: Webcam policy for a specific site. */
+ *title = g_strdup (_("Webcam Access Request"));
+ *message = g_strdup_printf (_("The page at “%s” would like to use your webcam"),
+ bold_origin);
+ break;
+ case EPHY_PERMISSION_TYPE_ACCESS_WEBCAM_AND_MICROPHONE:
+ /* Translators: Webcam and microphone policy for a specific site. */
+ *title = g_strdup (_("Webcam and Microphone Access Request"));
+ *message = g_strdup_printf (_("The page at “%s” would like to use your webcam and microphone"),
+ bold_origin);
+ break;
+ case EPHY_PERMISSION_TYPE_COOKIES:
+ requesting_domain = webkit_website_data_access_permission_request_get_requesting_domain (WEBKIT_WEBSITE_DATA_ACCESS_PERMISSION_REQUEST (self->permission_request));
+ current_domain = webkit_website_data_access_permission_request_get_current_domain (WEBKIT_WEBSITE_DATA_ACCESS_PERMISSION_REQUEST (self->permission_request));
+ /* Translators: Cookie policy for a specific site. */
+ *title = g_strdup (_("Third-party Cookies Request"));
+ *message = g_strdup_printf (_("The page at “%s” would like to use cookies while browsing “%s”. This will allow “%s” to track your activity."),
+ requesting_domain, current_domain, requesting_domain);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
diff --git a/src/ephy-permission-popover.h b/src/ephy-permission-popover.h
new file mode 100644
index 000000000..8f67a2c1b
--- /dev/null
+++ b/src/ephy-permission-popover.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2023 Jamie Murphy <hello@itsjamie.dev>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ephy-permissions-manager.h"
+
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+#include <webkit/webkit.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_PERMISSION_POPOVER (ephy_permission_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (EphyPermissionPopover, ephy_permission_popover, EPHY, PERMISSION_POPOVER, GtkPopover)
+
+EphyPermissionPopover *ephy_permission_popover_new (EphyPermissionType permission_type,
+ WebKitPermissionRequest *permission_request,
+ const char *origin);
+
+EphyPermissionType ephy_permission_popover_get_permission_type (EphyPermissionPopover *self);
+
+WebKitPermissionRequest *ephy_permission_popover_get_permission_request (EphyPermissionPopover *self);
+
+const char *ephy_permission_popover_get_origin (EphyPermissionPopover *self);
+
+void ephy_permission_popover_get_text (EphyPermissionPopover *self,
+ char **title,
+ char **message);
+
+G_END_DECLS
diff --git a/src/ephy-window.c b/src/ephy-window.c
index ae5c7a7ec..d815a1dc7 100644
--- a/src/ephy-window.c
+++ b/src/ephy-window.c
@@ -43,12 +43,15 @@
#include "ephy-fullscreen-box.h"
#include "ephy-gsb-utils.h"
#include "ephy-header-bar.h"
+#include "ephy-lib-type-builtins.h"
#include "ephy-link.h"
#include "ephy-location-entry.h"
#include "ephy-mouse-gesture-controller.h"
#include "ephy-pages-popover.h"
#include "ephy-pages-view.h"
+#include "ephy-password-popover.h"
#include "ephy-permissions-manager.h"
+#include "ephy-permission-popover.h"
#include "ephy-prefs.h"
#include "ephy-security-popover.h"
#include "ephy-session.h"
@@ -177,6 +180,7 @@ struct _EphyWindow {
GtkWidget *toast_overlay;
GtkWidget *switch_to_tab;
AdwToast *switch_toast;
+ GHashTable *active_permission_requests;
GList *pending_decisions;
gulong filters_initialized_id;
@@ -2646,6 +2650,187 @@ reader_mode_cb (EphyWebView *view,
}
static void
+load_all_available_popovers (EphyWindow *window,
+ EphyWebView *view)
+{
+ GList *popover_list = g_hash_table_lookup (window->active_permission_requests, view);
+ EphyTitleWidget *title_widget = ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (window->header_bar));
+ EphyLocationEntry *lentry;
+ GList *l;
+
+ if (!EPHY_IS_LOCATION_ENTRY (title_widget))
+ return;
+
+ lentry = EPHY_LOCATION_ENTRY (title_widget);
+
+ ephy_location_entry_set_password_popover (lentry, NULL);
+ ephy_location_entry_clear_permission_buttons (lentry);
+
+ for (l = popover_list; l; l = l->next) {
+ if (EPHY_IS_PASSWORD_POPOVER (l->data))
+ ephy_location_entry_set_password_popover (lentry, EPHY_PASSWORD_POPOVER (l->data));
+ else if (EPHY_IS_PERMISSION_POPOVER (l->data))
+ ephy_location_entry_add_permission_popover (lentry, EPHY_PERMISSION_POPOVER (l->data));
+ }
+}
+
+static void
+popover_response_cb (EphyWindow *window,
+ gpointer popover)
+{
+ EphyTitleWidget *title_widget = ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (window->header_bar));
+ EphyLocationEntry *lentry;
+ EphyWebView *view;
+ GList *popover_list;
+
+ if (!EPHY_IS_LOCATION_ENTRY (title_widget))
+ return;
+
+ lentry = EPHY_LOCATION_ENTRY (title_widget);
+
+ if (EPHY_IS_PASSWORD_POPOVER (popover))
+ ephy_location_entry_set_password_popover (lentry, NULL);
+
+ view = ephy_shell_get_active_web_view (ephy_shell_get_default ());
+ popover_list = g_hash_table_lookup (window->active_permission_requests, view);
+ popover_list = g_list_remove (popover_list, popover);
+
+ if (!popover_list)
+ g_hash_table_steal (window->active_permission_requests, view);
+ else
+ g_hash_table_replace (window->active_permission_requests, view, popover_list);
+
+ g_object_unref (popover);
+}
+
+static void
+set_permission (EphyPermissionPopover *popover,
+ gboolean response)
+{
+ EphyEmbedShell *shell = ephy_embed_shell_get_default ();
+ EphyPermissionsManager *permissions_manager = ephy_embed_shell_get_permissions_manager (shell);
+ EphyPermissionType permission_type = ephy_permission_popover_get_permission_type (popover);
+ const char *origin = ephy_permission_popover_get_origin (popover);
+
+ if (permission_type != (EPHY_PERMISSION_TYPE_ACCESS_WEBCAM_AND_MICROPHONE | EPHY_PERMISSION_TYPE_COOKIES)) {
+ ephy_permissions_manager_set_permission (permissions_manager,
+ permission_type, origin,
+ response ? EPHY_PERMISSION_PERMIT
+ : EPHY_PERMISSION_DENY);
+ } else {
+ ephy_permissions_manager_set_permission (permissions_manager,
+ EPHY_PERMISSION_TYPE_ACCESS_WEBCAM,
+ origin,
+ response ? EPHY_PERMISSION_PERMIT
+ : EPHY_PERMISSION_DENY);
+ ephy_permissions_manager_set_permission (permissions_manager,
+ EPHY_PERMISSION_TYPE_ACCESS_MICROPHONE,
+ origin,
+ response ? EPHY_PERMISSION_PERMIT
+ : EPHY_PERMISSION_DENY);
+ }
+
+ gtk_widget_unparent (GTK_WIDGET (popover));
+}
+
+static void
+on_permission_allow (AdwMessageDialog *self,
+ char *response,
+ EphyPermissionPopover *popover)
+{
+ webkit_permission_request_allow (ephy_permission_popover_get_permission_request (popover));
+ set_permission (popover, TRUE);
+}
+
+static void
+on_permission_deny (AdwMessageDialog *self,
+ char *response,
+ EphyPermissionPopover *popover)
+{
+ webkit_permission_request_deny (ephy_permission_popover_get_permission_request (popover));
+ set_permission (popover, FALSE);
+}
+
+static void
+popover_allow_cb (EphyPermissionPopover *popover,
+ EphyWindow *window)
+{
+ webkit_permission_request_allow (ephy_permission_popover_get_permission_request (popover));
+ set_permission (popover, TRUE);
+
+ popover_response_cb (window, popover);
+}
+
+static void
+popover_deny_cb (EphyPermissionPopover *popover,
+ EphyWindow *window)
+{
+ webkit_permission_request_deny (ephy_permission_popover_get_permission_request (popover));
+ set_permission (popover, FALSE);
+
+ popover_response_cb (window, popover);
+}
+
+static void
+permission_requested_cb (EphyWebView *view,
+ EphyPermissionType permission_type,
+ WebKitPermissionRequest *request,
+ const char *origin,
+ EphyWindow *window)
+{
+ EphyPermissionPopover *popover;
+
+ if (!gtk_widget_is_visible (GTK_WIDGET (window)))
+ return;
+
+ popover = ephy_permission_popover_new (permission_type, request, origin);
+
+ if ((ephy_embed_shell_get_mode (ephy_embed_shell_get_default ()) == EPHY_EMBED_SHELL_MODE_APPLICATION) ||
+ (window->adaptive_mode == EPHY_ADAPTIVE_MODE_NARROW)) {
+ AdwMessageDialog *dialog;
+ g_autofree char *title = NULL;
+ g_autofree char *message = NULL;
+
+ ephy_permission_popover_get_text (popover, &title, &message);
+ dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (GTK_WINDOW (window), title, message));
+
+ adw_message_dialog_add_responses (dialog,
+ "close", _("_Ask Later"),
+ "deny", _("_Deny"),
+ "allow", _("_Allow"),
+ NULL);
+
+ adw_message_dialog_set_body_use_markup (dialog, TRUE);
+ adw_message_dialog_set_response_appearance (dialog, "deny", ADW_RESPONSE_DESTRUCTIVE);
+ adw_message_dialog_set_response_appearance (dialog, "allow", ADW_RESPONSE_SUGGESTED);
+ adw_message_dialog_set_default_response (dialog, "close");
+ adw_message_dialog_set_close_response (dialog, "close");
+
+ g_signal_connect (dialog, "response::allow", G_CALLBACK (on_permission_allow), popover);
+ g_signal_connect (dialog, "response::deny", G_CALLBACK (on_permission_deny), popover);
+
+ gtk_window_present (GTK_WINDOW (dialog));
+ } else {
+ EphyTitleWidget *title_widget = ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (window->header_bar));
+ EphyLocationEntry *lentry;
+ GList *list = g_hash_table_lookup (EPHY_WINDOW (window)->active_permission_requests, view);
+
+ g_assert (EPHY_IS_LOCATION_ENTRY (title_widget));
+
+ g_object_ref_sink (popover);
+
+ lentry = EPHY_LOCATION_ENTRY (title_widget);
+ ephy_location_entry_add_permission_popover (lentry, popover);
+ ephy_location_entry_show_best_permission_popover (lentry);
+ list = g_list_append (list, popover);
+ g_hash_table_replace (EPHY_WINDOW (window)->active_permission_requests, view, list);
+
+ g_signal_connect (popover, "allow", G_CALLBACK (popover_allow_cb), window);
+ g_signal_connect (popover, "deny", G_CALLBACK (popover_deny_cb), window);
+ }
+}
+
+static void
tab_view_page_attached_cb (AdwTabView *tab_view,
AdwTabPage *page,
gint position,
@@ -2663,6 +2848,9 @@ tab_view_page_attached_cb (AdwTabView *tab_view,
g_signal_connect_object (ephy_embed_get_web_view (embed), "download-only-load",
G_CALLBACK (download_only_load_cb), window, G_CONNECT_AFTER);
+ g_signal_connect_object (ephy_embed_get_web_view (embed), "permission-requested",
+ G_CALLBACK (permission_requested_cb), window, G_CONNECT_AFTER);
+
g_signal_connect_object (ephy_embed_get_web_view (embed), "notify::reader-mode",
G_CALLBACK (reader_mode_cb), window, G_CONNECT_AFTER);
@@ -2689,6 +2877,8 @@ tab_view_page_detached_cb (AdwTabView *tab_view,
g_signal_handlers_disconnect_by_func
(ephy_embed_get_web_view (EPHY_EMBED (content)), G_CALLBACK (download_only_load_cb), window);
+ g_signal_handlers_disconnect_by_func
+ (ephy_embed_get_web_view (EPHY_EMBED (content)), G_CALLBACK (permission_requested_cb), window);
}
static void
@@ -2956,6 +3146,8 @@ tab_view_notify_selected_page_cb (EphyWindow *window)
ephy_window_set_active_tab (window, embed);
update_reader_mode (window, view);
+
+ load_all_available_popovers (window, view);
}
static EphyTabView *
@@ -3086,6 +3278,13 @@ is_browser_default (void)
}
static void
+free_permission_requests (gpointer key,
+ GList *value)
+{
+ g_list_free_full (value, g_object_unref);
+}
+
+static void
ephy_window_dispose (GObject *object)
{
EphyWindow *window = EPHY_WINDOW (object);
@@ -3105,6 +3304,11 @@ ephy_window_dispose (GObject *object)
g_clear_handle_id (&window->modified_forms_timeout_id, g_source_remove);
g_hash_table_unref (window->action_labels);
+
+ g_hash_table_foreach (window->active_permission_requests,
+ (GHFunc)free_permission_requests, NULL);
+
+ g_hash_table_unref (window->active_permission_requests);
}
G_OBJECT_CLASS (ephy_window_parent_class)->dispose (object);
@@ -3488,6 +3692,125 @@ download_completed_cb (EphyDownload *download,
}
static void
+on_username_entry_changed (GtkEditable *entry,
+ EphyPasswordRequestData *request_data)
+{
+ const char *text = gtk_editable_get_text (entry);
+ g_free (request_data->username);
+ request_data->username = g_strdup (text);
+}
+
+static void
+on_password_entry_changed (GtkEditable *entry,
+ EphyPasswordRequestData *request_data)
+{
+ const char *text = gtk_editable_get_text (entry);
+ g_free (request_data->password);
+ request_data->password = g_strdup (text);
+}
+
+static void
+on_password_never (AdwMessageDialog *dialog,
+ const char *response,
+ EphyPasswordRequestData *request_data)
+{
+ EphyEmbedShell *shell = EPHY_EMBED_SHELL (ephy_embed_shell_get_default ());
+ EphyPermissionsManager *permissions_manager = ephy_embed_shell_get_permissions_manager (shell);
+
+ ephy_permissions_manager_set_permission (permissions_manager,
+ EPHY_PERMISSION_TYPE_SAVE_PASSWORD,
+ request_data->origin,
+ EPHY_PERMISSION_DENY);
+}
+
+static void
+on_password_save (AdwMessageDialog *dialog,
+ const char *response,
+ EphyPasswordRequestData *request_data)
+{
+ EphyEmbedShell *shell = EPHY_EMBED_SHELL (ephy_embed_shell_get_default ());
+ EphyPasswordManager *password_manager = ephy_embed_shell_get_password_manager (shell);
+
+ ephy_password_manager_save (password_manager, request_data->origin,
+ request_data->target_origin, request_data->username,
+ request_data->password, request_data->usernameField,
+ request_data->passwordField, request_data->isNew);
+}
+
+static void
+save_password_cb (EphyEmbedShell *shell,
+ EphyPasswordRequestData *request_data)
+{
+ GtkWindow *window;
+ GtkWidget *title_widget;
+ EphyLocationEntry *lentry;
+
+ /* Sanity checks would have already occurred before this point */
+ window = gtk_application_get_active_window (GTK_APPLICATION (ephy_shell_get_default ()));
+ if (!gtk_widget_is_visible (GTK_WIDGET (window)))
+ return;
+
+ if ((ephy_embed_shell_get_mode (ephy_embed_shell_get_default ()) == EPHY_EMBED_SHELL_MODE_APPLICATION) ||
+ (EPHY_WINDOW (window)->adaptive_mode == EPHY_ADAPTIVE_MODE_NARROW)) {
+ AdwMessageDialog *dialog;
+ GtkBox *entry_box;
+ GtkWidget *username_entry;
+ GtkWidget *password_entry;
+
+ dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (GTK_WINDOW (window), _("Save login?"),
+ _("Passwords are saved only on your device and can be removed at any time in Preferences")));
+ entry_box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 6));
+ username_entry = gtk_entry_new ();
+ password_entry = gtk_password_entry_new ();
+
+ adw_message_dialog_add_responses (dialog,
+ "close", _("Not Now"),
+ "never", _("Never Save"),
+ "save", _("Save"),
+ NULL);
+
+ adw_message_dialog_set_response_appearance (dialog, "never", ADW_RESPONSE_DESTRUCTIVE);
+ adw_message_dialog_set_response_appearance (dialog, "save", ADW_RESPONSE_SUGGESTED);
+ adw_message_dialog_set_default_response (dialog, "close");
+ adw_message_dialog_set_close_response (dialog, "close");
+
+ gtk_editable_set_text (GTK_EDITABLE (username_entry), request_data->username);
+ gtk_editable_set_text (GTK_EDITABLE (password_entry), request_data->password);
+ gtk_password_entry_set_show_peek_icon (GTK_PASSWORD_ENTRY (password_entry), TRUE);
+
+ adw_message_dialog_set_extra_child (dialog, GTK_WIDGET (entry_box));
+ gtk_box_append (entry_box, username_entry);
+ gtk_box_append (entry_box, password_entry);
+
+ g_signal_connect (dialog, "response::save", G_CALLBACK (on_password_save), request_data);
+ g_signal_connect (dialog, "response::never", G_CALLBACK (on_password_never), request_data);
+ g_signal_connect (username_entry, "changed", G_CALLBACK (on_username_entry_changed), request_data);
+ g_signal_connect (password_entry, "changed", G_CALLBACK (on_password_entry_changed), request_data);
+
+ gtk_window_present (GTK_WINDOW (dialog));
+ } else {
+ EphyPasswordPopover *popover = ephy_password_popover_new (request_data);
+ EphyWebView *view = ephy_shell_get_active_web_view (EPHY_SHELL (shell));
+ GList *list = g_hash_table_lookup (EPHY_WINDOW (window)->active_permission_requests, view);
+
+ title_widget = GTK_WIDGET (ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (ephy_window_get_header_bar (EPHY_WINDOW (window)))));
+
+ g_assert (EPHY_IS_LOCATION_ENTRY (title_widget));
+
+ lentry = EPHY_LOCATION_ENTRY (title_widget);
+
+ g_object_ref_sink (popover);
+
+ ephy_location_entry_set_password_popover (lentry, popover);
+ ephy_location_entry_show_password_popover (lentry);
+ list = g_list_append (list, popover);
+ g_hash_table_replace (EPHY_WINDOW (window)->active_permission_requests, view, list);
+
+ g_signal_connect_swapped (popover, "response", G_CALLBACK (popover_response_cb), window);
+ }
+}
+
+static void
notify_leaflet_child_cb (EphyWindow *window)
{
GActionGroup *action_group;
@@ -3581,6 +3904,9 @@ ephy_window_constructed (GObject *object)
g_strdup (action_label[i].label));
}
+ window->active_permission_requests = g_hash_table_new (g_direct_hash,
+ g_direct_equal);
+
/* Set accels for actions */
app = g_application_get_default ();
for (i = 0; i < G_N_ELEMENTS (accels); i++) {
@@ -3733,6 +4059,7 @@ ephy_window_class_init (EphyWindowClass *klass)
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GtkWindowClass *window_class = GTK_WINDOW_CLASS (klass);
+ EphyShell *shell;
EphyDownloadsManager *manager;
object_class->constructed = ephy_window_constructed;
@@ -3766,8 +4093,10 @@ ephy_window_class_init (EphyWindowClass *klass)
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
- manager = ephy_embed_shell_get_downloads_manager (EPHY_EMBED_SHELL (ephy_shell_get_default ()));
+ shell = ephy_shell_get_default ();
+ manager = ephy_embed_shell_get_downloads_manager (EPHY_EMBED_SHELL (shell));
g_signal_connect (manager, "download-completed", G_CALLBACK (download_completed_cb), NULL);
+ g_signal_connect (shell, "password-form-submitted", G_CALLBACK (save_password_cb), NULL);
}
static void
diff --git a/src/meson.build b/src/meson.build
index 3e48be2d8..4edcccd1d 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -44,6 +44,8 @@ libephymain_sources = [
'ephy-page-row.c',
'ephy-pages-popover.c',
'ephy-pages-view.c',
+ 'ephy-permission-popover.c',
+ 'ephy-password-popover.c',
'ephy-security-popover.c',
'ephy-session.c',
'ephy-shell.c',
diff --git a/src/resources/ephy-permission-camera-symbolic.svg b/src/resources/ephy-permission-camera-symbolic.svg
new file mode 100644
index 000000000..f3f6564cd
--- /dev/null
+++ b/src/resources/ephy-permission-camera-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 7.023438 3.003906 c -1.644532 0 -3 1.355469 -3 3 v 1.425782 l -3.316407 -3.429688 h -0.6835935 v 8 h 0.6445315 l 3.355469 -3.496094 v 1.5 c 0 1.644532 1.355468 3 3 3 h 5 c 1.644531 0 3 -1.355468 3 -3 v -4 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 5 c 0.570312 0 1 0.429688 1 1 v 4 c 0 0.574219 -0.429688 1 -1 1 h -5 c -0.570313 0 -1 -0.425781 -1 -1 v -4 c 0 -0.570312 0.429687 -1 1 -1 z m 0 0" fill="#222222"/>
+</svg>
diff --git a/src/resources/ephy-permission-generic-symbolic.svg b/src/resources/ephy-permission-generic-symbolic.svg
new file mode 100644
index 000000000..89b383792
--- /dev/null
+++ b/src/resources/ephy-permission-generic-symbolic.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <g fill="#222222">
+ <path d="m 11.5 1 c -1.921875 0 -3.5 1.578125 -3.5 3.5 s 1.578125 3.5 3.5 3.5 s 3.5 -1.578125 3.5 -3.5 s -1.578125 -3.5 -3.5 -3.5 z m 0 2 c 0.839844 0 1.5 0.660156 1.5 1.5 s -0.660156 1.5 -1.5 1.5 s -1.5 -0.660156 -1.5 -1.5 s 0.660156 -1.5 1.5 -1.5 z m 0 0"/>
+ <path d="m 4.5 8 c -1.921875 0 -3.5 1.578125 -3.5 3.5 s 1.578125 3.5 3.5 3.5 c 1.386719 0 2.59375 -0.820312 3.15625 -2 h 5.84375 c 0.832031 0 1.5 -0.667969 1.5 -1.5 s -0.667969 -1.5 -1.5 -1.5 h -5.84375 c -0.5625 -1.179688 -1.769531 -2 -3.15625 -2 z m 0 2 c 0.839844 0 1.5 0.660156 1.5 1.5 s -0.660156 1.5 -1.5 1.5 s -1.5 -0.660156 -1.5 -1.5 s 0.660156 -1.5 1.5 -1.5 z m 0 0"/>
+ <path d="m 2.5 3 c -0.832031 0 -1.5 0.667969 -1.5 1.5 s 0.667969 1.5 1.5 1.5 h 4.769531 c -0.175781 -0.480469 -0.265625 -0.988281 -0.269531 -1.5 c 0 -0.511719 0.09375 -1.019531 0.269531 -1.5 z m 0 0" fill-opacity="0.34902"/>
+ </g>
+</svg>
diff --git a/src/resources/ephy-permission-location-symbolic.svg b/src/resources/ephy-permission-location-symbolic.svg
new file mode 100644
index 000000000..3e672d0bb
--- /dev/null
+++ b/src/resources/ephy-permission-location-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 8 2 c -2.761719 0 -5 2.238281 -5 5 c 0.003906 0.589844 0.113281 1.175781 0.320312 1.730469 h -0.011718 c 0.53125 1.652343 3.152344 4.039062 4.691406 5.578125 v -0.003906 c 1.257812 -1.253907 3.230469 -3.074219 4.203125 -4.601563 c 0.222656 -0.34375 0.390625 -0.671875 0.488281 -0.972656 h -0.015625 c 0.210938 -0.554688 0.320313 -1.140625 0.324219 -1.730469 c 0 -2.761719 -2.238281 -5 -5 -5 z m 0 3.078125 c 1.0625 0 1.921875 0.859375 1.921875 1.921875 c -0.007813 0.988281 -0.757813 1.808594 -1.742187 1.902344 l -0.53125 -0.019532 c -0.90625 -0.167968 -1.566407 -0.957031 -1.570313 -1.882812 c 0 -1.0625 0.859375 -1.921875 1.921875 -1.921875 z m 0 0" fill="#222222"/>
+</svg>
diff --git a/src/resources/ephy-permission-microphone-symbolic.svg b/src/resources/ephy-permission-microphone-symbolic.svg
new file mode 100644
index 000000000..ac1467110
--- /dev/null
+++ b/src/resources/ephy-permission-microphone-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 8 0 c -1.660156 0 -3 1.339844 -3 3 v 5 c 0 1.660156 1.339844 3 3 3 s 3 -1.339844 3 -3 v -5 c 0 -1.660156 -1.339844 -3 -3 -3 z m 0 2 c 0.554688 0 1 0.445312 1 1 v 5 c 0 0.554688 -0.445312 1 -1 1 s -1 -0.445312 -1 -1 v -5 c 0 -0.554688 0.445312 -1 1 -1 z m -6 5 v 1.011719 c 0 2.964843 2.164062 5.429687 5 5.90625 v 2.082031 h 2 v -2.082031 c 2.835938 -0.476563 5 -2.941407 5 -5.90625 v -1.011719 h -1.5 v 1.011719 c 0 2.5 -1.992188 4.488281 -4.5 4.488281 s -4.5 -1.988281 -4.5 -4.488281 v -1.011719 z m 0 0" fill="#222222"/>
+</svg>
diff --git a/src/resources/ephy-permission-notifications-symbolic.svg b/src/resources/ephy-permission-notifications-symbolic.svg
new file mode 100644
index 000000000..e4e4429b2
--- /dev/null
+++ b/src/resources/ephy-permission-notifications-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 8 1 c -0.710938 0.003906 -1.316406 0.523438 -1.4375 1.238281 c -1.542969 0.597657 -2.558594 2.085938 -2.5625 3.742188 c 0 0.007812 0 0.011719 0.003906 0.019531 h -0.003906 v 4 h -0.5 c -0.554688 0 -1 0.445312 -1 1 s 0.445312 1 1 1 h 9 c 0.554688 0 1 -0.445312 1 -1 s -0.445312 -1 -1 -1 h -0.5 v -4 c 0 -0.007812 0 -0.011719 0 -0.019531 c 0 -1.660157 -1.015625 -3.152344 -2.558594 -3.75 c -0.128906 -0.710938 -0.734375 -1.230469 -1.441406 -1.230469 z m -2 12 c 0 0.714844 0.382812 1.375 1 1.730469 c 0.617188 0.359375 1.382812 0.359375 2 0 c 0.617188 -0.355469 1 -1.015625 1 -1.730469 z m 0 0" fill="#222222"/>
+</svg>
diff --git a/src/resources/epiphany.gresource.xml b/src/resources/epiphany.gresource.xml
index 70f6cea6e..16458e790 100644
--- a/src/resources/epiphany.gresource.xml
+++ b/src/resources/epiphany.gresource.xml
@@ -23,7 +23,9 @@
<file preprocess="xml-stripblanks" compressed="true">gtk/page-row.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/pages-popover.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/pages-view.ui</file>
+ <file preprocess="xml-stripblanks" compressed="true">gtk/password-popover.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/passwords-view.ui</file>
+ <file preprocess="xml-stripblanks" compressed="true">gtk/permission-popover.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/prefs-appearance-page.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/prefs-dialog.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/prefs-extensions-page.ui</file>
@@ -56,6 +58,11 @@
<file compressed="true" alias="scalable/actions/ephy-download-done-symbolic.svg">ephy-download-done-symbolic.svg</file>
<file compressed="true" alias="scalable/actions/ephy-bookmark-tag-symbolic.svg">ephy-bookmark-tag-symbolic.svg</file>
<file compressed="true" alias="scalable/actions/ephy-reader-mode-symbolic.svg">ephy-reader-mode-symbolic.svg</file>
+ <file compressed="true" alias="scalable/actions/ephy-permission-camera-symbolic.svg">ephy-permission-camera-symbolic.svg</file>
+ <file compressed="true" alias="scalable/actions/ephy-permission-generic-symbolic.svg">ephy-permission-generic-symbolic.svg</file>
+ <file compressed="true" alias="scalable/actions/ephy-permission-location-symbolic.svg">ephy-permission-location-symbolic.svg</file>
+ <file compressed="true" alias="scalable/actions/ephy-permission-microphone-symbolic.svg">ephy-permission-microphone-symbolic.svg</file>
+ <file compressed="true" alias="scalable/actions/ephy-permission-notifications-symbolic.svg">ephy-permission-notifications-symbolic.svg</file>
<file compressed="true" alias="scalable/status/ephy-audio-muted-symbolic.svg">ephy-audio-muted-symbolic.svg</file>
<file compressed="true" alias="scalable/status/ephy-audio-playing-symbolic.svg">ephy-audio-playing-symbolic.svg</file>
<file compressed="true" alias="scalable/status/ephy-non-starred-symbolic.svg">ephy-non-starred-symbolic.svg</file>
diff --git a/src/resources/gtk/location-entry.ui b/src/resources/gtk/location-entry.ui
index 779772a35..aa92c4ea9 100644
--- a/src/resources/gtk/location-entry.ui
+++ b/src/resources/gtk/location-entry.ui
@@ -92,6 +92,18 @@
</object>
</child>
<child>
+ <object class="GtkMenuButton" id="password_button">
+ <property name="tooltip-text" translatable="yes">View available passwords</property>
+ <property name="valign">center</property>
+ <property name="visible">False</property>
+ <property name="icon-name">dialog-password-symbolic</property>
+ <style>
+ <class name="entry-icon"/>
+ <class name="end"/>
+ </style>
+ </object>
+ </child>
+ <child>
<object class="GtkPopover" id="suggestions_popover">
<property name="has-arrow">False</property>
<property name="autohide">False</property>
diff --git a/src/resources/gtk/password-popover.ui b/src/resources/gtk/password-popover.ui
new file mode 100644
index 000000000..4b2d6f3b4
--- /dev/null
+++ b/src/resources/gtk/password-popover.ui
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.16 -->
+ <template class="EphyPasswordPopover" parent="GtkPopover">
+ <style>
+ <class name="message-popover"/>
+ </style>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="xalign">0</property>
+ <property name="label">Save login?</property>
+ <property name="margin-bottom">6</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="username_entry">
+ <signal name="changed" handler="on_entry_changed" object="EphyPasswordPopover" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkPasswordEntry" id="password_entry">
+ <signal name="changed" handler="on_entry_changed" object="EphyPasswordPopover" swapped="yes"/>
+ <property name="show-peek-icon">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Passwords are saved only on your device and can be removed at any time in Preferences</property>
+ <property name="wrap">True</property>
+ <property name="margin-top">6</property>
+ <property name="max-width-chars">40</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <style>
+ <class name="response-area"/>
+ </style>
+ <child>
+ <object class="GtkButton" id="close_button">
+ <property name="label">_Never Save</property>
+ <property name="use-underline">True</property>
+ <property name="hexpand">True</property>
+ <signal name="clicked" handler="on_password_never" object="EphyPasswordPopover" swapped="yes"/>
+ <style>
+ <class name="flat"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="save_button">
+ <property name="label">_Save</property>
+ <property name="use-underline">True</property>
+ <property name="hexpand">True</property>
+ <signal name="clicked" handler="on_password_save" object="EphyPasswordPopover" swapped="yes"/>
+ <style>
+ <class name="flat"/>
+ <class name="suggested"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkSizeGroup">
+ <property name="mode">horizontal</property>
+ <widgets>
+ <widget name="close_button"/>
+ <widget name="save_button"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/src/resources/gtk/permission-popover.ui b/src/resources/gtk/permission-popover.ui
new file mode 100644
index 000000000..4b41e0882
--- /dev/null
+++ b/src/resources/gtk/permission-popover.ui
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.16 -->
+ <template class="EphyPermissionPopover" parent="GtkPopover">
+ <style>
+ <class name="message-popover"/>
+ </style>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="permission_title">
+ <property name="xalign">0</property>
+ <property name="margin-bottom">6</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="permission_description">
+ <property name="wrap">True</property>
+ <property name="max-width-chars">40</property>
+ <property name="use-markup">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <style>
+ <class name="response-area"/>
+ </style>
+ <child>
+ <object class="GtkButton" id="deny_button">
+ <property name="label">_Deny</property>
+ <property name="use-underline">True</property>
+ <property name="hexpand">True</property>
+ <signal name="clicked" handler="on_permission_deny" object="EphyPermissionPopover" swapped="yes"/>
+ <style>
+ <class name="flat"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="allow_button">
+ <property name="label">_Allow</property>
+ <property name="use-underline">True</property>
+ <property name="hexpand">True</property>
+ <signal name="clicked" handler="on_permission_allow" object="EphyPermissionPopover" swapped="yes"/>
+ <style>
+ <class name="flat"/>
+ <class name="suggested"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkSizeGroup">
+ <property name="mode">horizontal</property>
+ <widgets>
+ <widget name="deny_button"/>
+ <widget name="allow_button"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/src/resources/style-hc.css b/src/resources/style-hc.css
index e8d7f412e..f1973ffd6 100644
--- a/src/resources/style-hc.css
+++ b/src/resources/style-hc.css
@@ -13,3 +13,9 @@
popover.suggestions listview > row:selected {
box-shadow: inset 0 0 0 1px @borders;
}
+
+.response-area > button:hover,
+.response-area > button:active,
+.response-area > button:checked {
+ box-shadow: none;
+}
diff --git a/src/resources/style.css b/src/resources/style.css
index d682d4538..2d0ba1197 100644
--- a/src/resources/style.css
+++ b/src/resources/style.css
@@ -231,3 +231,36 @@ dnd > .boxed-list {
background-color: @window_bg_color;
background-image: image(@card_bg_color);
}
+
+.message-popover > contents {
+ padding: 0;
+ margin: 0;
+}
+
+.message-popover .response-area > button {
+ padding: 10px 14px;
+ border-radius: 0;
+ margin-top: -1px;
+ margin-right: -1px;
+ margin-left: -1px;
+}
+
+.message-popover .response-area > button.suggested {
+ color: @accent_color;
+}
+
+.message-popover .response-area > button.destructive {
+ color: @destructive_color;
+}
+
+.message-popover .response-area > button:first-child:dir(ltr),
+.message-popover .response-area > button:last-child:dir(rtl) {
+ border-bottom-left-radius: 12px;
+ margin-left: 0;
+}
+
+.message-popover .response-area > button:last-child:dir(ltr),
+.message-popover .response-area > button:first-child:dir(rtl) {
+ border-bottom-right-radius: 12px;
+ margin-right: 0;
+}