diff --git a/po/POTFILES.in b/po/POTFILES.in
index a11aa923..3a9d9fc5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -17,6 +17,12 @@ src/gui/common/gcal-calendar-row.c
src/gui/common/gcal-date-chooser.c
src/gui/event-editor/gcal-alarm-row.blp
src/gui/event-editor/gcal-alarm-row.c
+src/gui/event-editor/gcal-attendee-row.blp
+src/gui/event-editor/gcal-attendee-row.c
+src/gui/event-editor/gcal-attendee-details-page.blp
+src/gui/event-editor/gcal-attendee-details-page.c
+src/gui/event-editor/gcal-attendees-section.blp
+src/gui/event-editor/gcal-attendees-section.c
src/gui/event-editor/gcal-date-chooser-row.blp
src/gui/event-editor/gcal-date-time-chooser.blp
src/gui/event-editor/gcal-date-time-chooser.c
diff --git a/src/gui/event-editor/event-editor.gresource.xml b/src/gui/event-editor/event-editor.gresource.xml
index e32da094..92ed8298 100644
--- a/src/gui/event-editor/event-editor.gresource.xml
+++ b/src/gui/event-editor/event-editor.gresource.xml
@@ -13,5 +13,8 @@
gcal-summary-section.ui
gcal-time-zone-dialog.ui
gcal-time-zone-dialog-row.ui
+ gcal-attendees-section.ui
+ gcal-attendee-row.ui
+ gcal-attendee-details-page.ui
diff --git a/src/gui/event-editor/gcal-attendee-details-page.blp b/src/gui/event-editor/gcal-attendee-details-page.blp
new file mode 100644
index 00000000..1826007b
--- /dev/null
+++ b/src/gui/event-editor/gcal-attendee-details-page.blp
@@ -0,0 +1,81 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GcalAttendeeDetailsPage: Adw.NavigationPage {
+ title: _("Participants");
+
+ child: Adw.ToolbarView {
+ [top]
+ Adw.HeaderBar {
+ show-end-title-buttons: false;
+ show-start-title-buttons: true;
+ }
+
+ content: Adw.PreferencesPage {
+ Adw.PreferencesGroup {
+ title: _("Accepted");
+ visible: bind template.model-accepted as .n-items;
+
+ ListBox list_accepted {
+ selection-mode: none;
+
+ styles [
+ "boxed-list",
+ ]
+ }
+ }
+
+ Adw.PreferencesGroup {
+ title: _("Tentative");
+ visible: bind template.model-tentative as .n-items;
+
+ ListBox list_tentative {
+ selection-mode: none;
+
+ styles [
+ "boxed-list",
+ ]
+ }
+ }
+
+ Adw.PreferencesGroup {
+ title: _("Declined");
+ visible: bind template.model-declined as .n-items;
+
+ ListBox list_declined {
+ selection-mode: none;
+
+ styles [
+ "boxed-list",
+ ]
+ }
+ }
+
+ Adw.PreferencesGroup {
+ title: _("Delegated");
+ visible: bind template.model-delegated as .n-items;
+
+ ListBox list_delegated {
+ selection-mode: none;
+
+ styles [
+ "boxed-list",
+ ]
+ }
+ }
+
+ Adw.PreferencesGroup {
+ title: _("Not responded");
+ visible: bind template.model-not-responded as .n-items;
+
+ ListBox list_not_responded {
+ selection-mode: none;
+
+ styles [
+ "boxed-list",
+ ]
+ }
+ }
+ };
+ };
+}
diff --git a/src/gui/event-editor/gcal-attendee-details-page.c b/src/gui/event-editor/gcal-attendee-details-page.c
new file mode 100644
index 00000000..effbce3b
--- /dev/null
+++ b/src/gui/event-editor/gcal-attendee-details-page.c
@@ -0,0 +1,353 @@
+/* gcal-attendee-details-page.c
+ *
+ * Copyright (C) 2025 Alen Galinec
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gcal-attendee-details-page.h"
+#include "gcal-attendee-row.h"
+#include "gcal-enums.h"
+#include "gcal-event-attendee.h"
+#include "gcal-event.h"
+
+#include
+#include
+#include
+#include
+
+struct _GcalAttendeeDetailsPage
+{
+ AdwNavigationPage parent_instance;
+
+ GcalEvent *event;
+ GListStore *attendees;
+
+ GtkFilterListModel *filter_list_accepted;
+ GtkFilterListModel *filter_list_tentative;
+ GtkFilterListModel *filter_list_declined;
+ GtkFilterListModel *filter_list_delegated;
+ GtkFilterListModel *filter_list_not_responded;
+
+ GtkCustomFilter *accepted_filter;
+ GtkCustomFilter *tentative_filter;
+ GtkCustomFilter *declined_filter;
+ GtkCustomFilter *delegated_filter;
+ GtkCustomFilter *not_responded_filter;
+
+ GtkWidget *list_accepted;
+ GtkWidget *list_tentative;
+ GtkWidget *list_declined;
+ GtkWidget *list_delegated;
+ GtkWidget *list_not_responded;
+};
+
+enum
+{
+ PROP_MODEL_ACCEPTED = 1,
+ PROP_MODEL_TENTATIVE,
+ PROP_MODEL_DECLINED,
+ PROP_MODEL_DELEGATED,
+ PROP_MODEL_NOT_RESPONDED,
+ N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+G_DEFINE_FINAL_TYPE (GcalAttendeeDetailsPage, gcal_attendee_details_page, ADW_TYPE_NAVIGATION_PAGE)
+
+static void
+gcal_attendee_details_page_clear_list (GcalAttendeeDetailsPage *self)
+{
+ g_return_if_fail (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
+
+ g_list_store_remove_all (self->attendees);
+}
+
+static GtkWidget *
+create_attendee_row_widget (void *attendee,
+ void *user_data)
+{
+ return gcal_attendee_row_new (attendee);
+}
+
+static void
+gcal_attendee_details_page_finalize (GObject *object)
+{
+ GcalAttendeeDetailsPage *page = (GcalAttendeeDetailsPage *) object;
+
+ g_clear_pointer (&page->attendees, g_list_store_remove_all);
+
+ g_clear_object (&page->accepted_filter);
+ g_clear_object (&page->tentative_filter);
+ g_clear_object (&page->declined_filter);
+ g_clear_object (&page->delegated_filter);
+ g_clear_object (&page->not_responded_filter);
+
+ /* g_clear_pointer on these would try to free self->attendees again */
+ page->filter_list_accepted = NULL;
+ page->filter_list_tentative = NULL;
+ page->filter_list_declined = NULL;
+ page->filter_list_delegated = NULL;
+ page->filter_list_not_responded = NULL;
+}
+
+static gboolean
+custom_accepted_filter_cb (GcalEventAttendee *attendee,
+ gpointer data)
+{
+ return gcal_event_attendee_get_part_status (attendee) == GCAL_EVENT_ATTENDEE_PART_ACCEPTED;
+}
+
+static gboolean
+custom_tentative_filter_cb (GcalEventAttendee *attendee,
+ gpointer data)
+{
+ return gcal_event_attendee_get_part_status (attendee) == GCAL_EVENT_ATTENDEE_PART_TENTATIVE;
+}
+
+static gboolean
+custom_declined_filter_cb (GcalEventAttendee *attendee,
+ gpointer data)
+{
+ return gcal_event_attendee_get_part_status (attendee) == GCAL_EVENT_ATTENDEE_PART_DECLINED;
+}
+
+static gboolean
+custom_delegated_filter_cb (GcalEventAttendee *attendee,
+ gpointer data)
+{
+ return gcal_event_attendee_get_part_status (attendee) == GCAL_EVENT_ATTENDEE_PART_DELEGATED;
+}
+
+static gboolean
+custom_not_responded_filter_cb (GcalEventAttendee *attendee,
+ gpointer data)
+{
+ switch (gcal_event_attendee_get_part_status (attendee))
+ {
+ case GCAL_EVENT_ATTENDEE_PART_NONE:
+ case GCAL_EVENT_ATTENDEE_PART_NEEDS_ACTION:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static void
+gcal_attendee_details_page_init_filters (GcalAttendeeDetailsPage *self)
+{
+ self->accepted_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_accepted_filter_cb, NULL, NULL);
+ self->tentative_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_tentative_filter_cb, NULL, NULL);
+ self->declined_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_declined_filter_cb, NULL, NULL);
+ self->delegated_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_delegated_filter_cb, NULL, NULL);
+ self->not_responded_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_not_responded_filter_cb, NULL, NULL);
+}
+
+static void
+gcal_attendee_details_page_init_filter_models (GcalAttendeeDetailsPage *self)
+{
+ self->filter_list_accepted =
+ gtk_filter_list_model_new (G_LIST_MODEL (self->attendees),
+ GTK_FILTER (self->accepted_filter));
+
+ self->filter_list_tentative =
+ gtk_filter_list_model_new (G_LIST_MODEL (self->attendees),
+ GTK_FILTER (self->tentative_filter));
+
+ self->filter_list_declined =
+ gtk_filter_list_model_new (G_LIST_MODEL (self->attendees),
+ GTK_FILTER (self->declined_filter));
+
+ self->filter_list_delegated =
+ gtk_filter_list_model_new (G_LIST_MODEL (self->attendees),
+ GTK_FILTER (self->delegated_filter));
+
+ self->filter_list_not_responded =
+ gtk_filter_list_model_new (G_LIST_MODEL (self->attendees),
+ GTK_FILTER (self->not_responded_filter));
+}
+
+static void
+gcal_attendee_details_page_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GcalAttendeeDetailsPage *self = GCAL_ATTENDEE_DETAILS_PAGE (object);
+
+ switch (property_id)
+ {
+ case PROP_MODEL_ACCEPTED:
+ g_value_set_object (value, self->filter_list_accepted);
+ break;
+ case PROP_MODEL_TENTATIVE:
+ g_value_set_object (value, self->filter_list_tentative);
+ break;
+ case PROP_MODEL_DECLINED:
+ g_value_set_object (value, self->filter_list_declined);
+ break;
+ case PROP_MODEL_DELEGATED:
+ g_value_set_object (value, self->filter_list_delegated);
+ break;
+ case PROP_MODEL_NOT_RESPONDED:
+ g_value_set_object (value, self->filter_list_not_responded);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gcal_attendee_details_page_init (GcalAttendeeDetailsPage *instance)
+{
+ g_return_if_fail (GCAL_IS_ATTENDEE_DETAILS_PAGE (instance));
+
+ instance->attendees = g_list_store_new (GCAL_TYPE_EVENT_ATTENDEE);
+ gcal_attendee_details_page_init_filters (instance);
+ gcal_attendee_details_page_init_filter_models (instance);
+
+ gtk_widget_init_template (GTK_WIDGET (instance));
+
+ gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_accepted),
+ G_LIST_MODEL (instance->filter_list_accepted),
+ create_attendee_row_widget,
+ NULL, NULL);
+
+ gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_tentative),
+ G_LIST_MODEL (instance->filter_list_tentative),
+ create_attendee_row_widget,
+ NULL, NULL);
+
+ gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_declined),
+ G_LIST_MODEL (instance->filter_list_declined),
+ create_attendee_row_widget,
+ NULL, NULL);
+
+ gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_delegated),
+ G_LIST_MODEL (instance->filter_list_delegated),
+ create_attendee_row_widget,
+ NULL, NULL);
+
+ gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_not_responded),
+ G_LIST_MODEL (instance->filter_list_not_responded),
+ create_attendee_row_widget,
+ NULL, NULL);
+}
+
+static void
+gcal_attendee_details_page_class_init (GcalAttendeeDetailsPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ /* object */
+
+ object_class->finalize = gcal_attendee_details_page_finalize;
+ object_class->get_property = gcal_attendee_details_page_get_property;
+
+ /**
+ * GcalAttendeeDetailsPage:model-accepted:
+ *
+ * Binding model for the list of attendees with "accepted" response.
+ */
+ properties[PROP_MODEL_ACCEPTED] =
+ g_param_spec_object ("model-accepted", NULL, NULL,
+ GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeeDetailsPage:model-tentative:
+ *
+ * Binding model for the list of attendees with "tentative" (aka maybe) response.
+ */
+ properties[PROP_MODEL_TENTATIVE] =
+ g_param_spec_object ("model-tentative", NULL, NULL,
+ GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeeDetailsPage:model-declined:
+ *
+ * Binding model for the list of attendees with "declined" response.
+ */
+ properties[PROP_MODEL_DECLINED] =
+ g_param_spec_object ("model-declined", NULL, NULL,
+ GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeeDetailsPage:model-delegated:
+ *
+ * Binding model for the list of attendees with "delegated" response.
+ * Lists the attendees who delegated their attendance to someone else.
+ */
+ properties[PROP_MODEL_DELEGATED] =
+ g_param_spec_object ("model-delegated", NULL, NULL,
+ GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeeDetailsPage:model-not-responded:
+ *
+ * Binding model for the list of attendees who haven't responded yet
+ * or thei response type is unknown.
+ */
+ properties[PROP_MODEL_NOT_RESPONDED] =
+ g_param_spec_object ("model-not-responded", NULL, NULL,
+ GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ /* widget */
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-attendee-details-page.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_accepted);
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_tentative);
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_declined);
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_delegated);
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_not_responded);
+}
+
+/**
+ * gcal_attendee_details_page_set_event:
+ * @self: a #GcalAttendeeDetailsPage instance.
+ * @event: a #GcalEvent to use for this page.
+ *
+ * Sets the event for this page in a similar fashion to the #GcalEventEditorSection.
+ * Because this page is a reusable widget, the old event reference is replaced and
+ * the contents of this widget are reset while properly cleaning up resources.
+ */
+void
+gcal_attendee_details_page_set_event (GcalAttendeeDetailsPage *self,
+ GcalEvent *event)
+{
+ g_return_if_fail (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
+
+ if (self->event == event)
+ return;
+
+ gcal_attendee_details_page_clear_list (self);
+
+ g_set_object (&self->event, event);
+
+ if (self->event == NULL)
+ return;
+
+ g_autoptr (GSList) attendees = NULL;
+ g_autoptr (GSList) iter = NULL;
+
+ attendees = gcal_event_get_attendees (self->event);
+ for (iter = attendees; iter != NULL; iter = iter->next)
+ {
+ g_list_store_append (self->attendees, iter->data);
+ }
+}
diff --git a/src/gui/event-editor/gcal-attendee-details-page.h b/src/gui/event-editor/gcal-attendee-details-page.h
new file mode 100644
index 00000000..423cdac5
--- /dev/null
+++ b/src/gui/event-editor/gcal-attendee-details-page.h
@@ -0,0 +1,36 @@
+/* gcal-attendee-details-page.c
+ *
+ * Copyright (C) 2025 Alen Galinec
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef GCAL_ATTENDEE_DETAILS_PAGE_H
+#define GCAL_ATTENDEE_DETAILS_PAGE_H
+
+#include
+
+#include "gcal-event.h"
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_ATTENDEE_DETAILS_PAGE (gcal_attendee_details_page_get_type ())
+G_DECLARE_FINAL_TYPE (GcalAttendeeDetailsPage, gcal_attendee_details_page, GCAL, ATTENDEE_DETAILS_PAGE, AdwNavigationPage)
+
+void gcal_attendee_details_page_set_event (GcalAttendeeDetailsPage *self,
+ GcalEvent *event);
+
+G_END_DECLS
+
+#endif
diff --git a/src/gui/event-editor/gcal-attendee-row.blp b/src/gui/event-editor/gcal-attendee-row.blp
new file mode 100644
index 00000000..eff499b4
--- /dev/null
+++ b/src/gui/event-editor/gcal-attendee-row.blp
@@ -0,0 +1,32 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GcalAttendeeRow: Adw.ActionRow {
+ GestureClick gesture_click {
+ button: 3;
+ }
+
+ [suffix]
+ Box suffixes {
+ orientation: horizontal;
+
+ Label role {
+ styles [
+ "dimmed",
+ ]
+ }
+ }
+}
+
+PopoverMenu context_menu {
+ menu-model: context_menu_model;
+ has-arrow: false;
+ halign: start;
+}
+
+menu context_menu_model {
+ item {
+ label: _("Copy email");
+ action: "attendee-row.copy-email";
+ }
+}
diff --git a/src/gui/event-editor/gcal-attendee-row.c b/src/gui/event-editor/gcal-attendee-row.c
new file mode 100644
index 00000000..6b76a665
--- /dev/null
+++ b/src/gui/event-editor/gcal-attendee-row.c
@@ -0,0 +1,278 @@
+/* gcal-attendee-row.c
+ *
+ * Copyright (C) 2025 Alen Galinec
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gcal-attendee-row.h"
+#include "gcal-enums.h"
+#include "gcal-event-attendee.h"
+#include "gcal-utils.h"
+
+#include
+#include
+#include
+#include
+#include
+
+struct _GcalAttendeeRow
+{
+ AdwActionRow parent_instance;
+
+ GcalEventAttendee *attendee;
+
+ GtkWidget *role;
+ GtkWidget *context_menu;
+ GtkGestureClick *gesture_click;
+
+ GSimpleActionGroup *action_group;
+};
+
+G_DEFINE_FINAL_TYPE (GcalAttendeeRow, gcal_attendee_row, ADW_TYPE_ACTION_ROW);
+
+enum
+{
+ PROP_0,
+ PROP_ATTENDEE,
+ N_PROPS,
+};
+
+static GParamSpec *properties[N_PROPS];
+
+/* Aux functions */
+
+static void
+on_copy_email_cb (GSimpleAction *action,
+ GVariant *param,
+ gpointer user_data)
+{
+ GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (user_data);
+
+ GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (self));
+ GdkClipboard *clipboard = gdk_display_get_clipboard (display);
+
+ g_autofree const gchar *email =
+ gcal_get_email_from_mailto_uri (gcal_event_attendee_get_uri (self->attendee));
+
+ gdk_clipboard_set_text (clipboard, email);
+}
+
+static void
+show_context_menu (GcalAttendeeRow *self,
+ guint npress,
+ double x,
+ double y)
+{
+ g_return_if_fail (GCAL_IS_ATTENDEE_ROW (self));
+
+ const GdkRectangle position = { .x = x, .y = y };
+ gtk_popover_set_pointing_to (GTK_POPOVER (self->context_menu), &position);
+ gtk_popover_popup (GTK_POPOVER (self->context_menu));
+}
+
+static void
+setup_row_title_and_subtitle (GcalAttendeeRow *self)
+{
+ g_autoptr (GString) title = NULL;
+ g_autoptr (GString) subtitle = NULL;
+
+ /* this is the initial line for both cases */
+ title = g_string_new (gcal_event_attendee_get_name (self->attendee));
+ g_autofree const gchar *delegator_email = gcal_get_email_from_mailto_uri (gcal_event_attendee_get_uri (self->attendee));
+
+ if (gcal_event_attendee_get_part_status (self->attendee) != GCAL_EVENT_ATTENDEE_PART_DELEGATED)
+ {
+ /* show subtitle only when the name != email */
+ if (g_strcmp0 (gcal_event_attendee_get_name (self->attendee), delegator_email) != 0)
+ subtitle = g_string_new (delegator_email ? delegator_email : "");
+ }
+ else
+ {
+ /* append the delegator email to the title */
+ if (delegator_email)
+ g_string_append_printf (title, " (%s)", delegator_email);
+
+ g_autofree const gchar *delegated_to = gcal_get_email_from_mailto_uri (gcal_event_attendee_get_delegated_to (self->attendee));
+ subtitle = g_string_new (gettext ("Delegated to: "));
+ g_string_append (subtitle, delegated_to);
+ }
+
+ adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self), title->str);
+ adw_action_row_set_subtitle (ADW_ACTION_ROW (self), subtitle ? subtitle->str : NULL);
+}
+
+static void
+setup_row_content (GcalAttendeeRow *self)
+{
+ setup_row_title_and_subtitle (self);
+
+ /* set role */
+ if (gcal_event_attendee_get_role (self->attendee) == GCAL_EVENT_ATTENDEE_ROLE_OPTIONAL)
+ {
+ gtk_label_set_label (GTK_LABEL (self->role), gettext ("optional"));
+ }
+ else
+ {
+ /* other types don't need extra visual hints */
+ gtk_label_set_label (GTK_LABEL (self->role), NULL);
+ }
+}
+
+static void
+gcal_attendee_row_finalize (GObject *object)
+{
+ GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (object);
+
+ g_clear_object (&self->gesture_click);
+ g_clear_object (&self->action_group);
+
+ G_OBJECT_CLASS (gcal_attendee_row_parent_class)->finalize (object);
+}
+
+static void
+gcal_attendee_row_dispose (GObject *object)
+{
+ GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (object);
+ g_clear_pointer (&self->context_menu, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (gcal_attendee_row_parent_class)->dispose (object);
+}
+
+static void
+gcal_attendee_row_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (object);
+
+ switch (property_id)
+ {
+ case PROP_ATTENDEE:
+ g_assert (self->attendee == NULL);
+ self->attendee = g_value_get_object (value);
+ setup_row_content (self);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gcal_attendee_row_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (object);
+
+ switch (property_id)
+ {
+ case PROP_ATTENDEE:
+ g_value_set_object (value, self->attendee);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gcal_attendee_row_class_init (GcalAttendeeRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->set_property = gcal_attendee_row_set_property;
+ object_class->get_property = gcal_attendee_row_get_property;
+ object_class->dispose = gcal_attendee_row_dispose;
+ object_class->finalize = gcal_attendee_row_finalize;
+
+ /**
+ * GcalAttendeeRow:attendee:
+ *
+ * A reference to a #GcalEventAttendee for which this row is displayed.
+ */
+ properties[PROP_ATTENDEE] =
+ g_param_spec_object ("attendee", NULL, NULL,
+ GCAL_TYPE_EVENT_ATTENDEE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-attendee-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, context_menu);
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, gesture_click);
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, role);
+}
+
+static void
+gcal_attendee_row_init (GcalAttendeeRow *instance)
+{
+ static const GActionEntry actions[] = {
+ { "copy-email", on_copy_email_cb }
+ };
+
+ gtk_widget_init_template (GTK_WIDGET (instance));
+
+ gtk_widget_set_parent (GTK_WIDGET (instance->context_menu), GTK_WIDGET (instance));
+
+ instance->action_group = g_simple_action_group_new ();
+ g_action_map_add_action_entries (G_ACTION_MAP (instance->action_group),
+ actions,
+ G_N_ELEMENTS (actions),
+ instance);
+
+ gtk_widget_insert_action_group (instance->context_menu,
+ "attendee-row",
+ G_ACTION_GROUP (instance->action_group));
+
+ g_signal_connect_swapped (instance->gesture_click, "pressed", G_CALLBACK (show_context_menu), instance);
+}
+
+
+/**
+ * gcal_attendee_row_new:
+ * @attendee: an instance of #GcalEventAttendee to display.
+ *
+ * Creates a new #GcalAttendeeRow widget.
+ *
+ * Returns: a new #GcalAttendeeRow widget instance.
+ */
+GtkWidget *
+gcal_attendee_row_new (GcalEventAttendee *attendee)
+{
+ g_return_val_if_fail (GCAL_IS_EVENT_ATTENDEE (attendee), NULL);
+
+ return g_object_new (GCAL_TYPE_ATTENDEE_ROW,
+ "attendee", attendee,
+ NULL);
+}
+
+/**
+ * gcal_attendee_row_get_attendee:
+ * @self: a #GcalAttendeeRow
+ *
+ * Returns the internally stored reference to a #GcalEventAttendee.
+ *
+ * Returns: (transfer none) (nullable): the contained #GcalEventAttendee.
+ */
+GcalEventAttendee *
+gcal_attendee_row_get_attendee (GcalAttendeeRow *self)
+{
+ g_return_val_if_fail (GCAL_IS_ATTENDEE_ROW (self), NULL);
+
+ return self->attendee;
+}
diff --git a/src/gui/event-editor/gcal-attendee-row.h b/src/gui/event-editor/gcal-attendee-row.h
new file mode 100644
index 00000000..5ebe0cf5
--- /dev/null
+++ b/src/gui/event-editor/gcal-attendee-row.h
@@ -0,0 +1,33 @@
+/* gcal-attendee-row.h
+ *
+ * Copyright (C) 2025 Alen Galinec
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "gcal-event-attendee.h"
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_ATTENDEE_ROW (gcal_attendee_row_get_type ())
+G_DECLARE_FINAL_TYPE (GcalAttendeeRow, gcal_attendee_row, GCAL, ATTENDEE_ROW, AdwActionRow)
+
+GtkWidget *gcal_attendee_row_new (GcalEventAttendee *attendee);
+GcalEventAttendee *gcal_attendee_row_get_attendee (GcalAttendeeRow *self);
+
+G_END_DECLS
diff --git a/src/gui/event-editor/gcal-attendees-section.blp b/src/gui/event-editor/gcal-attendees-section.blp
new file mode 100644
index 00000000..e4bfdf22
--- /dev/null
+++ b/src/gui/event-editor/gcal-attendees-section.blp
@@ -0,0 +1,52 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GcalAttendeesSection: Gtk.Box {
+ orientation: vertical;
+ hexpand: true;
+ valign: start;
+
+ ListBox listbox {
+ halign: fill;
+ valign: fill;
+ selection-mode: none;
+
+ styles [
+ "boxed-list",
+ ]
+
+ Adw.ActionRow organizer_row {
+ title: _("Organizer");
+
+ styles [
+ "property",
+ ]
+ }
+
+ Adw.ActionRow summary_row {
+ title: _("Participants");
+ activatable-widget: open_details;
+ activated => $on_participant_summary_row_activated_cb(template) not-swapped;
+
+ styles [
+ "property",
+ ]
+
+ [suffix]
+ Label {
+ valign: center;
+ label: bind template.num-participants as ;
+ tooltip-text: _("Number of participants");
+
+ styles [
+ "participants-count-label",
+ ]
+ }
+
+ [suffix]
+ Image open_details {
+ icon-name: "go-next-symbolic";
+ }
+ }
+ }
+}
diff --git a/src/gui/event-editor/gcal-attendees-section.c b/src/gui/event-editor/gcal-attendees-section.c
new file mode 100644
index 00000000..f955d55d
--- /dev/null
+++ b/src/gui/event-editor/gcal-attendees-section.c
@@ -0,0 +1,412 @@
+/* gcal-attendees-section.c
+ *
+ * Copyright (C) 2025 Alen Galinec
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "gcal-attendees-section.h"
+
+#include
+#include
+#include
+
+#include "gcal-context.h"
+#include "gcal-enums.h"
+#include "gcal-event-attendee.h"
+#include "gcal-event-editor-section.h"
+#include "gcal-event-organizer.h"
+#include "gcal-event.h"
+#include "gcal-utils.h"
+
+struct _GcalAttendeesSection
+{
+ GtkBox parent_instance;
+
+ GcalContext *context;
+ GcalEvent *event;
+
+ AdwActionRow *organizer_row;
+ AdwActionRow *summary_row;
+
+ /* participation summary */
+ guint num_accepted;
+ guint num_declined;
+ guint num_tentative;
+ guint num_not_responded;
+ guint num_delegated;
+};
+
+static void gcal_event_editor_section_init_iface (GcalEventEditorSectionInterface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (GcalAttendeesSection, gcal_attendees_section, GTK_TYPE_BOX,
+ G_IMPLEMENT_INTERFACE (GCAL_TYPE_EVENT_EDITOR_SECTION, gcal_event_editor_section_init_iface))
+
+enum
+{
+ PROP_0,
+ PROP_CONTEXT,
+ PROP_NUM_ACCEPTED,
+ PROP_NUM_DECLINED,
+ PROP_NUM_TENTATIVE,
+ PROP_NUM_DELEGATED,
+ PROP_NUM_UNKNOWN,
+ PROP_NUM_TOTAL,
+ N_PROPS
+};
+
+enum
+{
+ SUMMARY_ROW_ACTIVATED,
+ N_SIGNALS,
+};
+
+static guint signals[N_SIGNALS] = { 0, };
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+static void
+reset_section (GcalAttendeesSection *self,
+ GcalEvent *event)
+{
+ if (event == self->event)
+ return;
+
+ g_set_object (&self->event, event);
+
+ adw_action_row_set_subtitle (self->organizer_row, NULL);
+ adw_action_row_set_subtitle (self->summary_row, NULL);
+
+ self->num_accepted = 0;
+ self->num_declined = 0;
+ self->num_tentative = 0;
+ self->num_not_responded = 0;
+ self->num_delegated = 0;
+}
+
+static inline void
+concat_subtitle_part (GStrvBuilder *builder,
+ guint value,
+ const gchar *text)
+{
+ if (value <= 0)
+ return;
+
+ g_strv_builder_take (builder, g_strdup_printf ("%d %s", value, text));
+}
+
+static gchar*
+build_summary_subtitle (GcalAttendeesSection *self)
+{
+ g_autoptr (GStrvBuilder) builder = g_strv_builder_new ();
+
+ concat_subtitle_part (builder, self->num_accepted, ngettext ("accepted", "accepted", self->num_accepted));
+ concat_subtitle_part (builder, self->num_tentative, ngettext ("maybe", "maybe", self->num_tentative));
+ concat_subtitle_part (builder, self->num_declined, ngettext ("declined", "declined", self->num_declined));
+ concat_subtitle_part (builder, self->num_delegated, ngettext ("delegated", "delegated", self->num_delegated));
+ concat_subtitle_part (builder, self->num_not_responded, ngettext ("not responded", "not responded", self->num_not_responded));
+
+ g_auto (GStrv) parts = g_strv_builder_end (builder);
+
+ gchar *result = g_strjoinv (", ", parts);
+
+ return result;
+}
+
+static void
+gcal_attendees_section_notify (GcalAttendeesSection *self)
+{
+ for (guint i = 1; i < N_PROPS; ++i)
+ g_object_notify_by_pspec (G_OBJECT (self), properties[i]);
+}
+
+static void
+gcal_attendees_section_set_event (GcalEventEditorSection *section,
+ GcalEvent *event,
+ GcalEventEditorFlags flags)
+{
+ GcalAttendeesSection *self = GCAL_ATTENDEES_SECTION (section);
+ GcalEventOrganizer *organizer;
+ g_autoptr (GSList) attendees = NULL;
+
+ reset_section (self, event);
+
+ if (self->event == NULL)
+ return;
+
+ /* set organizer */
+ organizer = gcal_event_get_organizer (self->event);
+ if (organizer)
+ {
+ g_autofree const gchar *email = gcal_get_email_from_mailto_uri (gcal_event_organizer_get_uri (organizer));
+ g_autofree const gchar *name_with_email = g_strdup_printf ("%s (%s)", gcal_event_organizer_get_name (organizer), email);
+
+ adw_action_row_set_subtitle (self->organizer_row, name_with_email);
+ }
+
+ /* set attendees and summarize attendance status */
+ attendees = gcal_event_get_attendees (self->event);
+ for (GSList *l = attendees; l != NULL; l = l->next)
+ {
+ g_autoptr (GcalEventAttendee) attendee = GCAL_EVENT_ATTENDEE (l->data);
+
+ switch (gcal_event_attendee_get_part_status (attendee))
+ {
+ case GCAL_EVENT_ATTENDEE_PART_ACCEPTED:
+ self->num_accepted++;
+ break;
+
+ case GCAL_EVENT_ATTENDEE_PART_DECLINED:
+ self->num_declined++;
+ break;
+
+ case GCAL_EVENT_ATTENDEE_PART_TENTATIVE:
+ self->num_tentative++;
+ break;
+
+ case GCAL_EVENT_ATTENDEE_PART_DELEGATED:
+ self->num_delegated++;
+ break;
+
+ default:
+ self->num_not_responded++;
+ break;
+ }
+ }
+
+ adw_action_row_set_subtitle (self->summary_row, build_summary_subtitle (self));
+
+ /* notify properties changed */
+ gcal_attendees_section_notify (self);
+}
+
+static void
+gcal_attendees_section_apply (GcalEventEditorSection *section)
+{
+ /* read-only */
+}
+
+static gboolean
+gcal_attendees_section_changed (GcalEventEditorSection *section)
+{
+ /* This section is read-only for now */
+ return FALSE;
+}
+
+static void
+gcal_event_editor_section_init_iface (GcalEventEditorSectionInterface *iface)
+{
+ iface->set_event = gcal_attendees_section_set_event;
+ iface->apply = gcal_attendees_section_apply;
+ iface->changed = gcal_attendees_section_changed;
+}
+
+static guint
+gcal_attendees_section_get_num_participants (GcalAttendeesSection *self)
+{
+ g_return_val_if_fail (GCAL_IS_ATTENDEES_SECTION (self), 0);
+
+ return self->num_accepted + self->num_declined + self->num_tentative + self->num_delegated + self->num_not_responded;
+}
+
+static void
+on_participant_summary_row_activated_cb (AdwActionRow *summary_row,
+ GcalAttendeesSection *self)
+{
+ g_signal_emit (self, signals[SUMMARY_ROW_ACTIVATED], 0);
+}
+
+static void
+gcal_attendees_section_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GcalAttendeesSection *self = GCAL_ATTENDEES_SECTION (object);
+
+ switch (property_id)
+ {
+ case PROP_CONTEXT:
+ g_value_set_object (value, self->context);
+ break;
+
+ case PROP_NUM_ACCEPTED:
+ g_value_set_uint (value, self->num_accepted);
+ break;
+
+ case PROP_NUM_DECLINED:
+ g_value_set_uint (value, self->num_declined);
+ break;
+
+ case PROP_NUM_TENTATIVE:
+ g_value_set_uint (value, self->num_tentative);
+ break;
+
+ case PROP_NUM_DELEGATED:
+ g_value_set_uint (value, self->num_delegated);
+ break;
+
+ case PROP_NUM_UNKNOWN:
+ g_value_set_uint (value, self->num_not_responded);
+ break;
+
+ case PROP_NUM_TOTAL:
+ g_value_set_uint (value, gcal_attendees_section_get_num_participants (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gcal_attendees_section_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GcalAttendeesSection *self = GCAL_ATTENDEES_SECTION (object);
+
+ switch (property_id)
+ {
+ case PROP_CONTEXT:
+ g_assert (self->context == NULL);
+ self->context = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gcal_attendees_section_finalize (GObject *object)
+{
+ GcalAttendeesSection *self = (GcalAttendeesSection *) object;
+
+ g_clear_object (&self->context);
+ g_clear_object (&self->event);
+
+ G_OBJECT_CLASS (gcal_attendees_section_parent_class)->finalize (object);
+}
+
+static void
+gcal_attendees_section_init (GcalAttendeesSection *instance)
+{
+ gtk_widget_init_template (GTK_WIDGET (instance));
+}
+
+static void
+gcal_attendees_section_class_init (GcalAttendeesSectionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gcal_attendees_section_finalize;
+ object_class->get_property = gcal_attendees_section_get_property;
+ object_class->set_property = gcal_attendees_section_set_property;
+
+ /**
+ * GcalAttendeesSection:context:
+ *
+ * The #GcalContext instance. Required by the interface.
+ */
+ properties[PROP_CONTEXT] =
+ g_param_spec_object ("context", NULL, NULL,
+ GCAL_TYPE_CONTEXT,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeesSection:num-accepted:
+ *
+ * Gets the number of attendees who accepted.
+ */
+ properties[PROP_NUM_ACCEPTED] =
+ g_param_spec_uint ("num-accepted", NULL, NULL,
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeesSection:num-declined:
+ *
+ * Gets the number of attendees who declined.
+ */
+ properties[PROP_NUM_DECLINED] =
+ g_param_spec_uint ("num-declined", NULL, NULL,
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeesSection:num-tentative:
+ *
+ * Gets the number of attendees who "maybe" will attend.
+ */
+ properties[PROP_NUM_TENTATIVE] =
+ g_param_spec_uint ("num-tentative", NULL, NULL,
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeesSection:num-unknown:
+ *
+ * Gets the number of attendees with an unknown response.
+ */
+ properties[PROP_NUM_UNKNOWN] =
+ g_param_spec_uint ("num-unknown", NULL, NULL,
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeesSection: num-delegated:
+ *
+ * Gets the number of attendees who delegated their attendance to someone else.
+ */
+ properties[PROP_NUM_DELEGATED] =
+ g_param_spec_uint ("num-delegated", NULL, NULL,
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GcalAttendeesSection:num-participants:
+ *
+ * Gets the total number of attendees/participants.
+ */
+ properties[PROP_NUM_TOTAL] =
+ g_param_spec_uint ("num-participants", NULL, NULL,
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ /**
+ * GcalAttendeesSection::summary-row-activated:
+ *
+ * This signal is emitted when the summary row is activated.
+ */
+ signals[SUMMARY_ROW_ACTIVATED] =
+ g_signal_new ("summary-row-activated",
+ GCAL_TYPE_ATTENDEES_SECTION,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-attendees-section.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeesSection, organizer_row);
+ gtk_widget_class_bind_template_child (widget_class, GcalAttendeesSection, summary_row);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_participant_summary_row_activated_cb);
+}
diff --git a/src/gui/event-editor/gcal-attendees-section.h b/src/gui/event-editor/gcal-attendees-section.h
new file mode 100644
index 00000000..ca78b0f4
--- /dev/null
+++ b/src/gui/event-editor/gcal-attendees-section.h
@@ -0,0 +1,29 @@
+/* gcal-attendees-section.h
+ *
+ * Copyright (C) 2025 Alen Galinec
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_ATTENDEES_SECTION (gcal_attendees_section_get_type ())
+G_DECLARE_FINAL_TYPE (GcalAttendeesSection, gcal_attendees_section, GCAL, ATTENDEES_SECTION, GtkBox)
+
+G_END_DECLS
diff --git a/src/gui/event-editor/gcal-event-editor-dialog.blp b/src/gui/event-editor/gcal-event-editor-dialog.blp
index 23e8e9ca..aa7f8572 100644
--- a/src/gui/event-editor/gcal-event-editor-dialog.blp
+++ b/src/gui/event-editor/gcal-event-editor-dialog.blp
@@ -5,89 +5,102 @@ template $GcalEventEditorDialog: Adw.Dialog {
content-width: 700;
content-height: 1000;
- Adw.ToolbarView {
- [top]
- Adw.HeaderBar {
- show-start-title-buttons: false;
- show-end-title-buttons: false;
+ Adw.NavigationView nav_view {
+ Adw.NavigationPage main_page {
+ Adw.ToolbarView {
+ [top]
+ Adw.HeaderBar {
+ show-start-title-buttons: false;
+ show-end-title-buttons: false;
- Button cancel_button {
- use-underline: true;
- label: _("_Cancel");
- clicked => $on_cancel_button_clicked_cb(template) not-swapped;
- }
-
- [end]
- Button done_button {
- use-underline: true;
- label: _("_Done");
- action-name: "event-editor.save";
-
- styles [
- "suggested-action",
- ]
- }
- }
-
- content: Box {
- orientation: vertical;
-
- Adw.Banner {
- title: _("This event is in a read-only calendar. It cannot be edited or deleted.");
- revealed: bind template.writable inverted;
- }
-
- Adw.PreferencesPage {
- vexpand: true;
-
- Adw.PreferencesGroup {
- $GcalSummarySection summary_section {
- sensitive: bind template.writable;
- }
- }
-
- Adw.PreferencesGroup {
- $GcalCalendarComboRow calendar_combo_row {
- sensitive: bind template.writable;
- }
- }
-
- Adw.PreferencesGroup {
- $GcalScheduleSection schedule_section {
- sensitive: bind template.writable;
- }
- }
-
- Adw.PreferencesGroup {
- title: _("Reminders");
-
- $GcalRemindersSection reminders_section {
- sensitive: bind template.writable;
- }
- }
-
- Adw.PreferencesGroup {
- title: _("Notes");
-
- $GcalNotesSection notes_section {
- sensitive: bind template.writable;
- vexpand: true;
- }
- }
-
- Adw.PreferencesGroup delete_group {
- Adw.ButtonRow {
- activated => $on_delete_row_activated_cb(template) not-swapped;
- title: _("_Delete Event");
+ Button cancel_button {
use-underline: true;
+ label: _("_Cancel");
+ clicked => $on_cancel_button_clicked_cb(template) not-swapped;
+ }
+
+ [end]
+ Button done_button {
+ use-underline: true;
+ label: _("_Done");
+ action-name: "event-editor.save";
styles [
- "destructive-action",
+ "suggested-action",
]
}
}
+
+ content: Box {
+ orientation: vertical;
+
+ Adw.Banner {
+ title: _("This event is in a read-only calendar. It cannot be edited or deleted.");
+ revealed: bind template.writable inverted;
+ }
+
+ Adw.PreferencesPage {
+ vexpand: true;
+
+ Adw.PreferencesGroup {
+ $GcalSummarySection summary_section {
+ sensitive: bind template.writable;
+ }
+ }
+
+ Adw.PreferencesGroup {
+ $GcalCalendarComboRow calendar_combo_row {
+ sensitive: bind template.writable;
+ }
+ }
+
+ Adw.PreferencesGroup {
+ $GcalScheduleSection schedule_section {
+ sensitive: bind template.writable;
+ }
+ }
+
+ Adw.PreferencesGroup participants_group {
+ $GcalAttendeesSection attendees_section {
+ sensitive: bind template.writable;
+ summary-row-activated => $on_attendees_summary_activated_cb(template) not-swapped;
+ }
+ }
+
+ Adw.PreferencesGroup {
+ title: _("Reminders");
+
+ $GcalRemindersSection reminders_section {
+ sensitive: bind template.writable;
+ }
+ }
+
+ Adw.PreferencesGroup {
+ title: _("Notes");
+
+ $GcalNotesSection notes_section {
+ sensitive: bind template.writable;
+ vexpand: true;
+ }
+ }
+
+ Adw.PreferencesGroup delete_group {
+ Adw.ButtonRow {
+ activated => $on_delete_row_activated_cb(template) not-swapped;
+ title: _("_Delete Event");
+ use-underline: true;
+
+ styles [
+ "destructive-action",
+ ]
+ }
+ }
+ }
+ };
}
- };
+ }
+
+ $GcalAttendeeDetailsPage attendee_details_page {}
}
ShortcutController {
diff --git a/src/gui/event-editor/gcal-event-editor-dialog.c b/src/gui/event-editor/gcal-event-editor-dialog.c
index a7ef425e..8137b4b6 100644
--- a/src/gui/event-editor/gcal-event-editor-dialog.c
+++ b/src/gui/event-editor/gcal-event-editor-dialog.c
@@ -19,22 +19,24 @@
#define G_LOG_DOMAIN "GcalEventEditorDialog"
-#include "gcal-alarm-row.h"
+#include "gcal-attendee-details-page.h"
+#include "gcal-attendees-section.h"
#include "gcal-calendar-combo-row.h"
#include "gcal-context.h"
#include "gcal-debug.h"
#include "gcal-event-editor-dialog.h"
#include "gcal-event-editor-section.h"
-#include "gcal-utils.h"
#include "gcal-event.h"
#include "gcal-notes-section.h"
#include "gcal-reminders-section.h"
#include "gcal-schedule-section.h"
#include "gcal-summary-section.h"
+#include "gcal-utils.h"
-#include
+#include
#include
#include
+#include
/**
* SECTION:gcal-event-editor-dialog
@@ -60,9 +62,14 @@ struct _GcalEventEditorDialog
GcalEventEditorSection *schedule_section;
GtkWidget *source_label;
GcalEventEditorSection *summary_section;
+ GcalEventEditorSection *attendees_section;
+ GtkWidget *participants_group;
+ GtkWidget *nav_view;
+ GtkWidget *main_page;
+ GtkWidget *attendee_details_page;
- GcalEventEditorSection *sections[4];
+ GcalEventEditorSection *sections[5];
GMenu *sources_menu;
@@ -302,6 +309,19 @@ out:
* Callbacks
*/
+static void
+on_attendees_summary_activated_cb (GcalAttendeesSection *section,
+ GcalEventEditorDialog *self)
+{
+ g_return_if_fail (GCAL_IS_EVENT_EDITOR_DIALOG (self));
+
+ gcal_attendee_details_page_set_event (GCAL_ATTENDEE_DETAILS_PAGE (self->attendee_details_page),
+ self->event);
+
+ adw_navigation_view_push (ADW_NAVIGATION_VIEW (self->nav_view),
+ ADW_NAVIGATION_PAGE (self->attendee_details_page));
+}
+
static void
on_event_editor_save_action_activated_cb (GSimpleAction *action,
GVariant *param,
@@ -522,6 +542,8 @@ gcal_event_editor_dialog_class_init (GcalEventEditorDialogClass *klass)
g_type_ensure (GCAL_TYPE_REMINDERS_SECTION);
g_type_ensure (GCAL_TYPE_SCHEDULE_SECTION);
g_type_ensure (GCAL_TYPE_SUMMARY_SECTION);
+ g_type_ensure (GCAL_TYPE_ATTENDEES_SECTION);
+ g_type_ensure (GCAL_TYPE_ATTENDEE_DETAILS_PAGE);
object_class->finalize = gcal_event_editor_dialog_finalize;
object_class->get_property = gcal_event_editor_dialog_get_property;
@@ -581,11 +603,16 @@ gcal_event_editor_dialog_class_init (GcalEventEditorDialogClass *klass)
gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, reminders_section);
gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, schedule_section);
gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, summary_section);
-
+ gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, attendees_section);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, participants_group);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, nav_view);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, main_page);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, attendee_details_page);
/* callbacks */
gtk_widget_class_bind_template_callback (widget_class, on_cancel_button_clicked_cb);
gtk_widget_class_bind_template_callback (widget_class, on_delete_row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_attendees_summary_activated_cb);
}
static void
@@ -608,6 +635,7 @@ gcal_event_editor_dialog_init (GcalEventEditorDialog *self)
self->sections[i++] = self->reminders_section;
self->sections[i++] = self->schedule_section;
self->sections[i++] = self->summary_section;
+ self->sections[i++] = self->attendees_section;
for (i = 0; i < G_N_ELEMENTS (self->sections); i++)
g_object_bind_property (self, "context", self->sections[i], "context", G_BINDING_DEFAULT);
@@ -666,11 +694,11 @@ gcal_event_editor_dialog_set_event (GcalEventEditorDialog *self,
/* dialog's title */
if (new_event)
- adw_dialog_set_title (ADW_DIALOG (self), _("New Event"));
+ adw_navigation_page_set_title (ADW_NAVIGATION_PAGE (self->main_page), _("New Event"));
else if (gcal_calendar_is_read_only (calendar))
- adw_dialog_set_title (ADW_DIALOG (self), _("Event Details"));
+ adw_navigation_page_set_title (ADW_NAVIGATION_PAGE (self->main_page), _("Event Details"));
else
- adw_dialog_set_title (ADW_DIALOG (self), _("Edit Event"));
+ adw_navigation_page_set_title (ADW_NAVIGATION_PAGE (self->main_page), _("Edit Event"));
g_list_store_remove_all (self->read_only_calendar_model);
if (gcal_calendar_is_read_only (calendar))
@@ -682,6 +710,8 @@ gcal_event_editor_dialog_set_event (GcalEventEditorDialog *self,
self->recurrence_changed = FALSE;
set_writable (self, !gcal_calendar_is_read_only (calendar));
+ gboolean has_participants = gcal_event_get_organizer (self->event) != NULL;
+ gtk_widget_set_visible (GTK_WIDGET (self->participants_group), has_participants);
gtk_widget_set_visible (self->cancel_button, self->writable);
diff --git a/src/gui/event-editor/meson.build b/src/gui/event-editor/meson.build
index 52d2c3d1..7032c132 100644
--- a/src/gui/event-editor/meson.build
+++ b/src/gui/event-editor/meson.build
@@ -16,6 +16,9 @@ blueprints = custom_target(
'gcal-time-chooser-row.blp',
'gcal-time-zone-dialog-row.blp',
'gcal-time-zone-dialog.blp',
+ 'gcal-attendees-section.blp',
+ 'gcal-attendee-row.blp',
+ 'gcal-attendee-details-page.blp',
),
output: '.',
command: [
@@ -49,4 +52,7 @@ sources += files(
'gcal-summary-section.c',
'gcal-time-zone-dialog.c',
'gcal-time-zone-dialog-row.c',
+ 'gcal-attendees-section.c',
+ 'gcal-attendee-row.c',
+ 'gcal-attendee-details-page.c',
)
diff --git a/src/theme/style.css b/src/theme/style.css
index c753cc58..0ecb9c1b 100644
--- a/src/theme/style.css
+++ b/src/theme/style.css
@@ -491,3 +491,10 @@ syncindicator {
.blurred {
filter: blur(6px);
}
+
+/* Participants section of the event editor dialog */
+.participants-count-label {
+ border-radius: 12px;
+ background-color: color-mix(in srgb, currentColor 10%, transparent);
+ padding: 6px 9px;
+}
diff --git a/src/utils/gcal-utils.c b/src/utils/gcal-utils.c
index bc21d743..f1bde2e4 100644
--- a/src/utils/gcal-utils.c
+++ b/src/utils/gcal-utils.c
@@ -1409,3 +1409,43 @@ gcal_create_writable_calendars_model (GcalManager *manager)
return G_LIST_MODEL (g_steal_pointer (&filter_model));
}
+/**
+ * gcal_get_email_from_mailto_uri:
+ * @mailto_uri: the original mailto string.
+ *
+ * Tries to strip the "mailto:" part of the incoming string.
+ * The incoming original URI should be in the form: "mailto:email.address@host.org"
+ * if it is coming from a valid iCal event.
+ *
+ * If the incoming string is malformed, i.e. contains more than one occurrence
+ * of the ":" (colon) character, the original string is returned.
+ * This may or may not be desirable.
+ *
+ * Returns: (transfer full) (nullable): The URI without the "mailto:" part.
+ */
+const gchar *
+gcal_get_email_from_mailto_uri (const gchar *mailto_uri)
+{
+ if (mailto_uri == NULL)
+ return NULL;
+
+ GStrv uri_pieces = g_strsplit (mailto_uri, ":", 0);
+
+ if (g_strv_length (uri_pieces) != 2)
+ {
+ g_strfreev (uri_pieces);
+ g_warning ("Malformed organizer URI (value): %s", mailto_uri);
+
+ /* note: There was a crash here! Copying is important
+ * because the original string may be owned by someone else.
+ * (The "mailto_uri" from GcalEventAttendee and GcalEventOrganizer are not transfered) */
+
+ /* return original string */
+ return g_strdup (mailto_uri);
+ }
+
+ gchar *address = g_strdup (uri_pieces[1]);
+ g_strfreev (uri_pieces);
+
+ return address;
+}
diff --git a/src/utils/gcal-utils.h b/src/utils/gcal-utils.h
index 70235d07..f867cf2e 100644
--- a/src/utils/gcal-utils.h
+++ b/src/utils/gcal-utils.h
@@ -136,4 +136,5 @@ SoupSession * gcal_create_soup_session (void);
GListModel * gcal_create_writable_calendars_model (GcalManager *manager);
+const gchar * gcal_get_email_from_mailto_uri (const gchar *mailto_uri);
#endif /* __GCAL_UTILS_H__ */