Invite people to events on Federated Core calendars

This commit is contained in:
2026-10-29 08:18:52 -06:00
parent eeea79bb9f
commit 8d9eb034c7
24 changed files with 1324 additions and 19 deletions
+247
View File
@@ -2023,3 +2023,250 @@ gcal_event_get_organizer (GcalEvent *self)
return self->organizer;
}
static gboolean
mailto_equal (const gchar *uri1,
const gchar *uri2)
{
if (!uri1 || !uri2)
return FALSE;
if (g_ascii_strncasecmp (uri1, "mailto:", 7) == 0)
uri1 += 7;
if (g_ascii_strncasecmp (uri2, "mailto:", 7) == 0)
uri2 += 7;
return g_ascii_strcasecmp (uri1, uri2) == 0;
}
static void
set_component_attendees (GcalEvent *self,
GSList *ecal_attendees)
{
e_cal_component_set_attendees (self->component, ecal_attendees);
e_cal_component_commit_sequence (self->component);
g_list_store_remove_all (self->attendees);
load_attendees (self);
}
/**
* gcal_event_set_organizer:
* @self: a #GcalEvent
* @email: (nullable): the email address of the organizer
* @name: (nullable): the common name of the organizer
*
* Sets the organizer of @self. Passing %NULL as @email removes the
* organizer.
*/
void
gcal_event_set_organizer (GcalEvent *self,
const gchar *email,
const gchar *name)
{
ECalComponentOrganizer *organizer = NULL;
g_return_if_fail (GCAL_IS_EVENT (self));
g_clear_object (&self->organizer);
if (email && *email)
{
g_autofree gchar *uri = g_strdup_printf ("mailto:%s", email);
organizer = e_cal_component_organizer_new_full (uri, NULL, (name && *name) ? name : NULL, NULL);
self->organizer = gcal_event_organizer_new (organizer);
}
e_cal_component_set_organizer (self->component, organizer);
e_cal_component_commit_sequence (self->component);
e_cal_component_organizer_free (organizer);
}
/**
* gcal_event_has_attendee:
* @self: a #GcalEvent
* @email: an email address, with or without the "mailto:" prefix
*
* Returns: %TRUE if @email is one of the attendees of @self
*/
gboolean
gcal_event_has_attendee (GcalEvent *self,
const gchar *email)
{
guint n_items;
g_return_val_if_fail (GCAL_IS_EVENT (self), FALSE);
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->attendees));
for (guint i = 0; i < n_items; i++)
{
g_autoptr (GcalEventAttendee) attendee = g_list_model_get_item (G_LIST_MODEL (self->attendees), i);
if (mailto_equal (gcal_event_attendee_get_uri (attendee), email))
return TRUE;
}
return FALSE;
}
/**
* gcal_event_add_attendee:
* @self: a #GcalEvent
* @email: the email address of the attendee
* @name: (nullable): the common name of the attendee
*
* Adds a person to the attendees of @self, who is asked to respond.
*
* Returns: %FALSE if @email already was an attendee
*/
gboolean
gcal_event_add_attendee (GcalEvent *self,
const gchar *email,
const gchar *name)
{
g_autofree gchar *uri = NULL;
GSList *ecal_attendees;
g_return_val_if_fail (GCAL_IS_EVENT (self), FALSE);
g_return_val_if_fail (email && *email, FALSE);
if (gcal_event_has_attendee (self, email))
return FALSE;
uri = g_strdup_printf ("mailto:%s", email);
ecal_attendees = e_cal_component_get_attendees (self->component);
ecal_attendees = g_slist_append (ecal_attendees,
e_cal_component_attendee_new_full (uri,
NULL,
I_CAL_CUTYPE_INDIVIDUAL,
I_CAL_ROLE_REQPARTICIPANT,
I_CAL_PARTSTAT_NEEDSACTION,
TRUE,
NULL,
NULL,
NULL,
(name && *name) ? name : NULL,
NULL));
set_component_attendees (self, ecal_attendees);
g_slist_free_full (ecal_attendees, e_cal_component_attendee_free);
return TRUE;
}
/**
* gcal_event_remove_attendee:
* @self: a #GcalEvent
* @email: an email address, with or without the "mailto:" prefix
*
* Removes @email from the attendees of @self.
*/
void
gcal_event_remove_attendee (GcalEvent *self,
const gchar *email)
{
GSList *ecal_attendees;
GSList *l;
g_return_if_fail (GCAL_IS_EVENT (self));
ecal_attendees = e_cal_component_get_attendees (self->component);
for (l = ecal_attendees; l != NULL; l = l->next)
{
if (mailto_equal (e_cal_component_attendee_get_value (l->data), email))
break;
}
if (l)
{
e_cal_component_attendee_free (l->data);
ecal_attendees = g_slist_delete_link (ecal_attendees, l);
set_component_attendees (self, ecal_attendees);
}
g_slist_free_full (ecal_attendees, e_cal_component_attendee_free);
}
/**
* gcal_event_copy_attendees:
* @self: a #GcalEvent
* @event: the #GcalEvent to copy from
*
* Replaces the organizer and attendees of @self with those of @event.
*/
void
gcal_event_copy_attendees (GcalEvent *self,
GcalEvent *event)
{
ECalComponentOrganizer *organizer;
GSList *ecal_attendees;
g_return_if_fail (GCAL_IS_EVENT (self));
g_return_if_fail (GCAL_IS_EVENT (event));
organizer = e_cal_component_get_organizer (event->component);
g_clear_object (&self->organizer);
if (organizer)
self->organizer = gcal_event_organizer_new (organizer);
e_cal_component_set_organizer (self->component, organizer);
ecal_attendees = e_cal_component_get_attendees (event->component);
set_component_attendees (self, ecal_attendees);
g_slist_free_full (ecal_attendees, e_cal_component_attendee_free);
e_cal_component_organizer_free (organizer);
}
/**
* gcal_event_attendees_equal:
* @event1: a #GcalEvent
* @event2: a #GcalEvent
*
* Checks if @event1 and @event2 have the same organizer and attendees.
* Only the email addresses are compared.
*
* Returns: %TRUE if the events' organizer and attendees are identical
*/
gboolean
gcal_event_attendees_equal (GcalEvent *event1,
GcalEvent *event2)
{
const gchar *organizer1 = NULL;
const gchar *organizer2 = NULL;
guint n_items;
g_assert (GCAL_IS_EVENT (event1) && GCAL_IS_EVENT (event2));
if (event1->organizer)
organizer1 = gcal_event_organizer_get_uri (event1->organizer);
if (event2->organizer)
organizer2 = gcal_event_organizer_get_uri (event2->organizer);
if ((organizer1 || organizer2) && !mailto_equal (organizer1, organizer2))
return FALSE;
n_items = g_list_model_get_n_items (G_LIST_MODEL (event1->attendees));
if (n_items != g_list_model_get_n_items (G_LIST_MODEL (event2->attendees)))
return FALSE;
for (guint i = 0; i < n_items; i++)
{
g_autoptr (GcalEventAttendee) attendee = g_list_model_get_item (G_LIST_MODEL (event1->attendees), i);
if (!gcal_event_has_attendee (event2, gcal_event_attendee_get_uri (attendee)))
return FALSE;
}
return TRUE;
}
+20
View File
@@ -142,6 +142,26 @@ GListModel * gcal_event_get_attendees (GcalEvent
GcalEventOrganizer * gcal_event_get_organizer (GcalEvent *self);
void gcal_event_set_organizer (GcalEvent *self,
const gchar *email,
const gchar *name);
gboolean gcal_event_has_attendee (GcalEvent *self,
const gchar *email);
gboolean gcal_event_add_attendee (GcalEvent *self,
const gchar *email,
const gchar *name);
void gcal_event_remove_attendee (GcalEvent *self,
const gchar *email);
void gcal_event_copy_attendees (GcalEvent *self,
GcalEvent *event);
gboolean gcal_event_attendees_equal (GcalEvent *event1,
GcalEvent *event2);
gboolean gcal_event_alarms_equal (GcalEvent *event1,
GcalEvent *event2);
+255
View File
@@ -0,0 +1,255 @@
/* gcal-federated-core.c
*
* Copyright (C) 2026 David Young <david@federated.computer>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#define G_LOG_DOMAIN "GcalFederatedCore"
#include "config.h"
#include "gcal-federated-core.h"
#include <libedataserver/libedataserver.h>
#include <string.h>
/**
* SECTION:gcal-federated-core
* @short_description: Federated Computer Core integration
* @title:GcalFederatedCore
*
* When CoreDesktop's `configure` has connected this computer to a Federated
* Computer Core, each Core is described in ~/.config/coredesktop/cores/<domain>.conf
* and its Nextcloud calendars are added through GNOME Online Accounts.
*
* Nextcloud schedules events on the server: when an event whose organizer
* is the calendar's owner is saved with attendees, Nextcloud emails the
* invitations, updates and cancellations. So inviting people to an event on
* a Core calendar only takes setting its organizer and attendees.
*/
typedef struct
{
gchar *domain;
gchar *email;
gchar *name;
} CoreConf;
static void
core_conf_free (CoreConf *conf)
{
g_free (conf->domain);
g_free (conf->email);
g_free (conf->name);
g_free (conf);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (CoreConf, core_conf_free)
/*
* The files are written by bash with printf %q, e.g. CORE_NAME=David\ Young,
* which g_shell_unquote() reads.
*/
static CoreConf *
load_core_conf (const gchar *path)
{
g_autoptr (CoreConf) conf = NULL;
g_autofree gchar *contents = NULL;
g_auto (GStrv) lines = NULL;
if (!g_file_get_contents (path, &contents, NULL, NULL))
return NULL;
conf = g_new0 (CoreConf, 1);
lines = g_strsplit (contents, "\n", -1);
for (gint i = 0; lines[i]; i++)
{
g_autofree gchar *value = NULL;
gchar *equals;
gchar **field;
equals = strchr (lines[i], '=');
if (!equals)
continue;
*equals = '\0';
if (g_str_equal (lines[i], "CORE_DOMAIN"))
field = &conf->domain;
else if (g_str_equal (lines[i], "CORE_EMAIL"))
field = &conf->email;
else if (g_str_equal (lines[i], "CORE_NAME"))
field = &conf->name;
else
continue;
value = g_shell_unquote (equals + 1, NULL);
if (value)
g_set_str (field, value);
}
if (!conf->domain || !*conf->domain || !conf->email || !strchr (conf->email, '@'))
return NULL;
return g_steal_pointer (&conf);
}
static GPtrArray *
load_cores (const gchar *config_dir)
{
g_autofree gchar *cores_dir = NULL;
g_autoptr (GPtrArray) cores = NULL;
g_autoptr (GDir) dir = NULL;
const gchar *filename;
cores = g_ptr_array_new_with_free_func ((GDestroyNotify) core_conf_free);
cores_dir = g_build_filename (config_dir, "cores", NULL);
dir = g_dir_open (cores_dir, 0, NULL);
while (dir && (filename = g_dir_read_name (dir)) != NULL)
{
g_autofree gchar *path = NULL;
CoreConf *conf;
if (!g_str_has_suffix (filename, ".conf"))
continue;
path = g_build_filename (cores_dir, filename, NULL);
conf = load_core_conf (path);
if (conf)
g_ptr_array_add (cores, conf);
}
/* Before CoreDesktop supported several Cores, there was only core.conf */
if (cores->len == 0)
{
g_autofree gchar *path = g_build_filename (config_dir, "core.conf", NULL);
CoreConf *conf = load_core_conf (path);
if (conf)
g_ptr_array_add (cores, conf);
}
return g_steal_pointer (&cores);
}
/**
* gcal_federated_core_find_core:
* @config_dir: CoreDesktop's configuration directory
* @host: the host name of a CalDAV server
* @out_email: (out) (optional): return location for the Core email
* @out_name: (out) (optional): return location for the name
*
* Looks for the Core whose Nextcloud is at @host.
*
* Returns: %TRUE if @host is the Nextcloud of a Core set up on this computer
*/
gboolean
gcal_federated_core_find_core (const gchar *config_dir,
const gchar *host,
gchar **out_email,
gchar **out_name)
{
g_autoptr (GPtrArray) cores = NULL;
g_return_val_if_fail (config_dir != NULL, FALSE);
if (!host)
return FALSE;
cores = load_cores (config_dir);
for (guint i = 0; i < cores->len; i++)
{
CoreConf *conf = g_ptr_array_index (cores, i);
g_autofree gchar *nextcloud_host = g_strconcat ("nextcloud.", conf->domain, NULL);
if (g_ascii_strcasecmp (host, nextcloud_host) != 0)
continue;
if (out_email)
*out_email = g_strdup (conf->email);
if (out_name)
*out_name = g_strdup (conf->name);
return TRUE;
}
return FALSE;
}
/**
* gcal_federated_core_get_organizer:
* @calendar: a #GcalCalendar
* @out_email: (out) (optional): return location for the organizer's email
* @out_name: (out) (optional): return location for the organizer's name
*
* Checks whether @calendar is on the Nextcloud of a Federated Computer Core
* set up on this computer, where people can be invited to events, and who
* the organizer of those events is.
*
* Returns: %TRUE if people can be invited to events in @calendar
*/
gboolean
gcal_federated_core_get_organizer (GcalCalendar *calendar,
gchar **out_email,
gchar **out_name)
{
#ifdef ENABLE_FEDERATED_CORE
g_autofree gchar *config_dir = NULL;
g_autofree gchar *server_email = NULL;
g_autofree gchar *core_email = NULL;
g_autofree gchar *core_name = NULL;
g_autoptr (GUri) uri = NULL;
ESourceWebdav *webdav;
ESource *source;
g_return_val_if_fail (GCAL_IS_CALENDAR (calendar), FALSE);
if (gcal_calendar_is_read_only (calendar))
return FALSE;
source = gcal_calendar_get_source (calendar);
if (!source || !e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND))
return FALSE;
webdav = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
uri = e_source_webdav_dup_uri (webdav);
/* CoreDesktop writes to ~/.config, even where XDG_CONFIG_HOME is elsewhere (e.g. Flatpak) */
config_dir = g_build_filename (g_get_home_dir (), ".config", "coredesktop", NULL);
if (!uri || !gcal_federated_core_find_core (config_dir, g_uri_get_host (uri), &core_email, &core_name))
return FALSE;
/*
* Nextcloud only sends invitations when the organizer is one of the
* owner's addresses. The CalDAV server says which address that is, so
* prefer it to the Core email.
*/
server_email = e_source_webdav_dup_email_address (webdav);
if (out_email)
*out_email = (server_email && *server_email) ? g_steal_pointer (&server_email) : g_steal_pointer (&core_email);
if (out_name)
*out_name = g_steal_pointer (&core_name);
return TRUE;
#else
return FALSE;
#endif
}
+34
View File
@@ -0,0 +1,34 @@
/* gcal-federated-core.h
*
* Copyright (C) 2026 David Young <david@federated.computer>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "gcal-calendar.h"
G_BEGIN_DECLS
gboolean gcal_federated_core_get_organizer (GcalCalendar *calendar,
gchar **out_email,
gchar **out_name);
gboolean gcal_federated_core_find_core (const gchar *config_dir,
const gchar *host,
gchar **out_email,
gchar **out_name);
G_END_DECLS
+5
View File
@@ -0,0 +1,5 @@
calendar_incs += include_directories('.')
sources += files(
'gcal-federated-core.c',
)
@@ -54,6 +54,8 @@ struct _GcalAttendeeDetailsPage
GtkWidget *group_not_responded;
GcalEventAttendeeTypeFilterFlags attendee_type_filter_flags;
gboolean removable;
};
enum
@@ -64,6 +66,7 @@ enum
PROP_MODEL_DELEGATED,
PROP_MODEL_NOT_RESPONDED,
PROP_ATTENDEE_TYPE,
PROP_REMOVABLE,
N_PROPS
};
@@ -83,7 +86,11 @@ static GtkWidget *
create_attendee_row_widget (void *attendee,
void *user_data)
{
return gcal_attendee_row_new (attendee);
GtkWidget *row = gcal_attendee_row_new (attendee);
g_object_bind_property (user_data, "removable", row, "removable", G_BINDING_SYNC_CREATE);
return row;
}
static void
@@ -237,6 +244,9 @@ gcal_attendee_details_page_get_property (GObject *object,
case PROP_ATTENDEE_TYPE:
g_value_set_flags (value, self->attendee_type_filter_flags);
break;
case PROP_REMOVABLE:
g_value_set_boolean (value, self->removable);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
@@ -255,6 +265,9 @@ gcal_attendee_details_page_set_property (GObject *object,
case PROP_ATTENDEE_TYPE:
set_type_filter_flags (self, g_value_get_flags (value));
break;
case PROP_REMOVABLE:
gcal_attendee_details_page_set_removable (self, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
@@ -275,27 +288,27 @@ gcal_attendee_details_page_init (GcalAttendeeDetailsPage *instance)
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_accepted),
G_LIST_MODEL (instance->filter_list_accepted),
create_attendee_row_widget,
NULL, NULL);
instance, NULL);
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_tentative),
G_LIST_MODEL (instance->filter_list_tentative),
create_attendee_row_widget,
NULL, NULL);
instance, NULL);
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_declined),
G_LIST_MODEL (instance->filter_list_declined),
create_attendee_row_widget,
NULL, NULL);
instance, NULL);
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_delegated),
G_LIST_MODEL (instance->filter_list_delegated),
create_attendee_row_widget,
NULL, NULL);
instance, NULL);
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_not_responded),
G_LIST_MODEL (instance->filter_list_not_responded),
create_attendee_row_widget,
NULL, NULL);
instance, NULL);
}
static void
@@ -370,6 +383,16 @@ gcal_attendee_details_page_class_init (GcalAttendeeDetailsPageClass *klass)
GCAL_EVENT_ATTENDEE_TYPE_FILTER_NONE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
/**
* GcalAttendeeDetailsPage:removable:
*
* Whether attendees can be removed from the event.
*/
properties[PROP_REMOVABLE] =
g_param_spec_boolean ("removable", NULL, NULL,
FALSE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_PROPS, properties);
/* widget */
@@ -420,3 +443,24 @@ gcal_attendee_details_page_set_type_filter (GcalAttendeeDetailsPage *sel
g_assert (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
set_type_filter_flags (self, flags);
}
/**
* gcal_attendee_details_page_set_removable:
* @self: a #GcalAttendeeDetailsPage instance.
* @removable: whether attendees can be removed
*
* Sets whether each attendee is shown with a button that removes them
* from the event.
*/
void
gcal_attendee_details_page_set_removable (GcalAttendeeDetailsPage *self,
gboolean removable)
{
g_assert (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
if (self->removable == removable)
return;
self->removable = removable;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_REMOVABLE]);
}
@@ -34,6 +34,9 @@ void gcal_attendee_details_page_set_attendees (GcalAtte
void gcal_attendee_details_page_set_type_filter (GcalAttendeeDetailsPage *self,
GcalEventAttendeeTypeFilterFlags flags);
void gcal_attendee_details_page_set_removable (GcalAttendeeDetailsPage *self,
gboolean removable);
G_END_DECLS
#endif
@@ -12,6 +12,18 @@ template $GcalAttendeeRow: Adw.ActionRow {
"dimmed",
]
}
[suffix]
Button remove_button {
visible: bind template.removable;
valign: center;
icon-name: "list-remove-symbolic";
tooltip-text: _("Remove Participant");
styles [
"flat",
]
}
}
PopoverMenu context_menu {
+28
View File
@@ -34,10 +34,13 @@ struct _GcalAttendeeRow
GcalEventAttendee *attendee;
GtkWidget *role;
GtkWidget *remove_button;
GtkWidget *context_menu;
GtkGestureClick *gesture_click;
GSimpleActionGroup *action_group;
gboolean removable;
};
G_DEFINE_FINAL_TYPE (GcalAttendeeRow, gcal_attendee_row, ADW_TYPE_ACTION_ROW);
@@ -46,6 +49,7 @@ enum
{
PROP_0,
PROP_ATTENDEE,
PROP_REMOVABLE,
N_PROPS,
};
@@ -164,6 +168,16 @@ gcal_attendee_row_set_property (GObject *object,
g_assert (self->attendee == NULL);
self->attendee = g_value_get_object (value);
set_up_row_content (self);
gtk_actionable_set_action_target (GTK_ACTIONABLE (self->remove_button), "s",
gcal_event_attendee_get_uri (self->attendee) ? gcal_event_attendee_get_uri (self->attendee) : "");
gtk_actionable_set_action_name (GTK_ACTIONABLE (self->remove_button), "event-editor.remove-attendee");
break;
case PROP_REMOVABLE:
if (self->removable != g_value_get_boolean (value))
{
self->removable = g_value_get_boolean (value);
g_object_notify_by_pspec (object, pspec);
}
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -183,6 +197,9 @@ gcal_attendee_row_get_property (GObject *object,
case PROP_ATTENDEE:
g_value_set_object (value, self->attendee);
break;
case PROP_REMOVABLE:
g_value_set_boolean (value, self->removable);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
@@ -209,12 +226,23 @@ gcal_attendee_row_class_init (GcalAttendeeRowClass *klass)
GCAL_TYPE_EVENT_ATTENDEE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
/**
* GcalAttendeeRow:removable:
*
* Whether the row offers to remove the attendee from the event.
*/
properties[PROP_REMOVABLE] =
g_param_spec_boolean ("removable", NULL, NULL,
FALSE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | 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, remove_button);
gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, role);
}
@@ -11,6 +11,16 @@ template $GcalAttendeesSection: $GcalEventEditorSection {
filter-flags: GCAL_EVENT_ATTENDEE_TYPE_FILTER_PERSON;
}
Adw.EntryRow invite_row {
title: _("Invite by Email");
visible: bind template.can-invite;
input-purpose: email;
show-apply-button: true;
apply => $on_invite_row_apply_cb(template) not-swapped;
entry-activated => $on_invite_row_apply_cb(template) not-swapped;
changed => $on_invite_row_changed_cb(template) not-swapped;
}
$GcalAttendeeSummaryRow {
title: _("Resources");
attendees: bind template.attendees;
+167 -9
View File
@@ -27,17 +27,25 @@
#include "gcal-event-organizer.h"
#include "gcal-event.h"
#include "gcal-organizer-row.h"
#include "gcal-utils.h"
#include "glib-object.h"
struct _GcalAttendeesSection
{
GcalEventEditorSection parent_instance;
GcalEvent *event;
GcalEventOrganizer *organizer;
GtkWidget *invite_row;
GtkWidget *organizer_row;
AdwActionRow *summary_row;
GListModel *attendees;
/* Who invitations come from; NULL when people can't be invited */
gchar *inviter_email;
gchar *inviter_name;
};
G_DEFINE_FINAL_TYPE (GcalAttendeesSection, gcal_attendees_section, GCAL_TYPE_EVENT_EDITOR_SECTION)
@@ -46,6 +54,7 @@ enum
{
PROP_0,
PROP_ATTENDEES,
PROP_CAN_INVITE,
PROP_ORGANIZER,
N_PROPS
};
@@ -53,6 +62,97 @@ enum
static GParamSpec *properties[N_PROPS] = { NULL, };
/*
* Auxiliary methods
*/
static void
update_from_event (GcalAttendeesSection *self)
{
self->organizer = self->event ? gcal_event_get_organizer (self->event) : NULL;
self->attendees = self->event ? gcal_event_get_attendees (self->event) : NULL;
gtk_widget_set_visible (self->organizer_row, self->organizer != NULL);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ATTENDEES]);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ORGANIZER]);
}
static gboolean
invite (GcalAttendeesSection *self,
const gchar *text)
{
g_autofree gchar *email = NULL;
g_autofree gchar *name = NULL;
if (!gcal_util_parse_email_address (text, &email, &name))
return FALSE;
/* The organizer doesn't invite themselves */
if (g_ascii_strcasecmp (email, self->inviter_email) == 0)
return TRUE;
if (!gcal_event_get_organizer (self->event))
gcal_event_set_organizer (self->event, self->inviter_email, self->inviter_name);
gcal_event_add_attendee (self->event, email, name);
return TRUE;
}
/*
* Callbacks
*/
static void
on_invite_row_apply_cb (AdwEntryRow *row,
GcalAttendeesSection *self)
{
g_autoptr (GStrvBuilder) not_invited = NULL;
g_auto (GStrv) addresses = NULL;
g_auto (GStrv) remaining = NULL;
g_autofree gchar *remaining_text = NULL;
if (!self->event || !self->inviter_email)
return;
/* Several addresses can be pasted at once */
addresses = g_strsplit_set (gtk_editable_get_text (GTK_EDITABLE (row)), ",;\n", -1);
not_invited = g_strv_builder_new ();
for (gint i = 0; addresses[i]; i++)
{
g_strstrip (addresses[i]);
if (*addresses[i] && !invite (self, addresses[i]))
g_strv_builder_add (not_invited, addresses[i]);
}
/* Leave what isn't an email address, so it can be corrected */
remaining = g_strv_builder_end (not_invited);
remaining_text = g_strjoinv (", ", remaining);
gtk_editable_set_text (GTK_EDITABLE (row), remaining_text);
if (*remaining_text)
gtk_widget_add_css_class (GTK_WIDGET (row), "error");
}
static void
on_invite_row_changed_cb (AdwEntryRow *row,
GcalAttendeesSection *self)
{
gtk_widget_remove_css_class (GTK_WIDGET (row), "error");
}
static void
on_attendees_changed_cb (GcalAttendeesSection *self)
{
update_from_event (self);
}
/*
* GcalEventEditorSection overrides
*/
@@ -63,19 +163,23 @@ gcal_attendees_section_event_set_cb (GcalEventEditorSection *section,
{
GcalAttendeesSection *self = GCAL_ATTENDEES_SECTION (section);
gtk_editable_set_text (GTK_EDITABLE (self->invite_row), "");
if (self->event)
g_signal_handlers_disconnect_by_func (gcal_event_get_attendees (self->event), on_attendees_changed_cb, self);
g_set_weak_pointer (&self->event, event);
if (event)
{
self->organizer = gcal_event_get_organizer (event);
self->attendees = gcal_event_get_attendees (event);
}
else
{
self->organizer = NULL;
self->attendees = NULL;
g_signal_connect_object (gcal_event_get_attendees (event),
"items-changed",
G_CALLBACK (on_attendees_changed_cb),
self,
G_CONNECT_SWAPPED);
}
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ATTENDEES]);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ORGANIZER]);
update_from_event (self);
}
@@ -97,6 +201,10 @@ gcal_attendees_section_get_property (GObject *object,
g_value_set_object (value, self->attendees);
break;
case PROP_CAN_INVITE:
g_value_set_boolean (value, self->inviter_email != NULL);
break;
case PROP_ORGANIZER:
g_value_set_object (value, self->organizer);
break;
@@ -132,6 +240,13 @@ gcal_attendees_section_finalize (GObject *object)
self->attendees = NULL; /* not owned */
if (self->event)
g_signal_handlers_disconnect_by_func (gcal_event_get_attendees (self->event), on_attendees_changed_cb, self);
g_clear_weak_pointer (&self->event);
g_clear_pointer (&self->inviter_email, g_free);
g_clear_pointer (&self->inviter_name, g_free);
G_OBJECT_CLASS (gcal_attendees_section_parent_class)->finalize (object);
}
@@ -168,6 +283,17 @@ gcal_attendees_section_class_init (GcalAttendeesSectionClass *klass)
G_TYPE_LIST_MODEL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
/**
* GcalAttendeesSection:can-invite:
*
* Whether people can be invited to the event.
* See gcal_attendees_section_set_inviter().
*/
properties[PROP_CAN_INVITE] =
g_param_spec_boolean ("can-invite", NULL, NULL,
FALSE,
G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* GcalAttendeesSection:organizer:
*
@@ -183,5 +309,37 @@ gcal_attendees_section_class_init (GcalAttendeesSectionClass *klass)
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, invite_row);
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_invite_row_apply_cb);
gtk_widget_class_bind_template_callback (widget_class, on_invite_row_changed_cb);
}
/**
* gcal_attendees_section_set_inviter:
* @self: a #GcalAttendeesSection
* @email: (nullable): the email address invitations come from
* @name: (nullable): the name invitations come from
*
* Lets people be invited to the event, on behalf of @email, who becomes
* its organizer. Passing %NULL as @email stops people from being invited.
*/
void
gcal_attendees_section_set_inviter (GcalAttendeesSection *self,
const gchar *email,
const gchar *name)
{
gboolean could_invite;
g_return_if_fail (GCAL_IS_ATTENDEES_SECTION (self));
could_invite = self->inviter_email != NULL;
g_set_str (&self->inviter_email, email);
g_set_str (&self->inviter_name, email ? name : NULL);
if (could_invite != (self->inviter_email != NULL))
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CAN_INVITE]);
}
@@ -27,4 +27,8 @@ G_BEGIN_DECLS
#define GCAL_TYPE_ATTENDEES_SECTION (gcal_attendees_section_get_type ())
G_DECLARE_FINAL_TYPE (GcalAttendeesSection, gcal_attendees_section, GCAL, ATTENDEES_SECTION, GcalEventEditorSection)
void gcal_attendees_section_set_inviter (GcalAttendeesSection *self,
const gchar *email,
const gchar *name);
G_END_DECLS
@@ -28,6 +28,7 @@
#include "gcal-event-editor-dialog.h"
#include "gcal-event-editor-section.h"
#include "gcal-event.h"
#include "gcal-federated-core.h"
#include "gcal-notes-section.h"
#include "gcal-reminders-section.h"
#include "gcal-schedule-section.h"
@@ -128,6 +129,48 @@ set_writable (GcalEventEditorDialog *self,
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WRITABLE]);
}
/*
* People can be invited to events on the calendars of a Federated Computer
* Core, which sends the invitations, unless someone else organizes the event.
*/
static void
update_invite_state (GcalEventEditorDialog *self)
{
g_autofree gchar *email = NULL;
g_autofree gchar *name = NULL;
GcalEventOrganizer *organizer;
GcalCalendar *calendar;
gboolean can_invite = FALSE;
if (!self->edited_event)
return;
calendar = gcal_calendar_combo_row_get_calendar (self->calendar_combo_row);
if (!calendar)
calendar = gcal_event_get_calendar (self->edited_event);
organizer = gcal_event_get_organizer (self->edited_event);
if (self->writable && calendar && gcal_federated_core_get_organizer (calendar, &email, &name))
{
g_autofree const gchar *organizer_email = NULL;
if (organizer)
organizer_email = gcal_util_get_email_from_mailto_uri (gcal_event_organizer_get_uri (organizer));
can_invite = !organizer_email || g_ascii_strcasecmp (organizer_email, email) == 0;
}
gcal_attendees_section_set_inviter (GCAL_ATTENDEES_SECTION (self->attendees_section),
can_invite ? email : NULL,
can_invite ? name : NULL);
gcal_attendee_details_page_set_removable (GCAL_ATTENDEE_DETAILS_PAGE (self->attendee_details_page), can_invite);
gtk_widget_action_set_enabled (GTK_WIDGET (self), "event-editor.remove-attendee", can_invite);
gtk_widget_set_visible (GTK_WIDGET (self->attendees_section), organizer != NULL || can_invite);
}
static void
set_event (GcalEventEditorDialog *self,
GcalEvent *event,
@@ -165,8 +208,7 @@ set_event (GcalEventEditorDialog *self,
gcal_calendar_combo_row_set_calendar (self->calendar_combo_row, calendar);
set_writable (self, !gcal_calendar_is_read_only (calendar));
gboolean has_participants = gcal_event_get_organizer (self->edited_event) != NULL;
gtk_widget_set_visible (GTK_WIDGET (self->attendees_section), has_participants);
update_invite_state (self);
gtk_widget_set_visible (self->cancel_button, self->writable);
@@ -235,6 +277,9 @@ apply_event_properties_to_template_event (GcalEvent *template_event,
if (alarm)
gcal_event_add_alarm (template_event, alarm);
}
if (!gcal_event_attendees_equal (template_event, event))
gcal_event_copy_attendees (template_event, event);
}
static void
@@ -275,7 +320,7 @@ save_event (GcalEventEditorDialog *self,
GcalContext *context;
GcalManager *manager;
gboolean calendar_equal, alarms_equal, schedule_equal, strings_equal, event_equal;
gboolean calendar_equal, alarms_equal, schedule_equal, strings_equal, attendees_equal, event_equal;
GCAL_ENTRY;
@@ -284,11 +329,20 @@ save_event (GcalEventEditorDialog *self,
selected_calendar = gcal_calendar_combo_row_get_calendar (self->calendar_combo_row);
/* Everyone invited here was removed again, so there's nothing to organize */
if (!gcal_event_get_organizer (self->event) &&
gcal_event_get_organizer (self->edited_event) &&
g_list_model_get_n_items (gcal_event_get_attendees (self->edited_event)) == 0)
{
gcal_event_set_organizer (self->edited_event, NULL, NULL);
}
calendar_equal = selected_calendar && calendar == selected_calendar;
alarms_equal = gcal_event_alarms_equal (self->event, self->edited_event);
schedule_equal = gcal_event_schedule_equal (self->event, self->edited_event);
strings_equal = gcal_event_strings_equal (self->event, self->edited_event);
event_equal = calendar_equal && alarms_equal && schedule_equal && strings_equal;
attendees_equal = gcal_event_attendees_equal (self->event, self->edited_event);
event_equal = calendar_equal && alarms_equal && schedule_equal && strings_equal && attendees_equal;
if (self->event_is_new)
{
@@ -357,6 +411,25 @@ on_show_attendees_detail_page_action_activated_cb (GtkWidget *widget,
ADW_NAVIGATION_PAGE (self->attendee_details_page));
}
static void
on_remove_attendee_action_activated_cb (GtkWidget *widget,
const char *action_name,
GVariant *param)
{
GcalEventEditorDialog *self = GCAL_EVENT_EDITOR_DIALOG (widget);
gcal_event_remove_attendee (self->edited_event, g_variant_get_string (param, NULL));
if (g_list_model_get_n_items (self->attendees_model) == 0)
adw_navigation_view_pop (ADW_NAVIGATION_VIEW (self->nav_view));
}
static void
on_calendar_selected_cb (GcalEventEditorDialog *self)
{
update_invite_state (self);
}
static void
on_cancel_button_clicked_cb (GtkButton *button,
GcalEventEditorDialog *self)
@@ -638,6 +711,8 @@ gcal_event_editor_dialog_class_init (GcalEventEditorDialogClass *klass)
gtk_widget_class_install_action (widget_class, "event-editor.save", NULL, on_event_editor_save_action_activated_cb);
gtk_widget_class_install_action (widget_class, "event-editor.show-attendees-detail-page", "u",
on_show_attendees_detail_page_action_activated_cb);
gtk_widget_class_install_action (widget_class, "event-editor.remove-attendee", "s",
on_remove_attendee_action_activated_cb);
}
static void
@@ -648,6 +723,7 @@ gcal_event_editor_dialog_init (GcalEventEditorDialog *self)
set_up_context (self);
g_signal_connect_swapped (self, "validation-requested", G_CALLBACK (on_validation_requested_cb), self);
g_signal_connect_swapped (self->calendar_combo_row, "notify::selected-item", G_CALLBACK (on_calendar_selected_cb), self);
}
/**
@@ -89,6 +89,10 @@ generate_subtitle (GcalOrganizerRow *self,
return NULL;
email = gcal_util_get_email_from_mailto_uri (gcal_event_organizer_get_uri (organizer));
if (!gcal_event_organizer_get_name (organizer))
return g_strdup (email);
name_with_email = g_strdup_printf ("%s (%s)", gcal_event_organizer_get_name (organizer), email);
return g_strdup (name_with_email);
+1
View File
@@ -75,6 +75,7 @@ sources = files()
subdir('appdata')
subdir('contrib')
subdir('core')
subdir('federated')
subdir('gui')
subdir('search')
subdir('theme')
+87
View File
@@ -1081,6 +1081,93 @@ gcal_util_get_email_from_mailto_uri (const gchar *mailto_uri)
return address;
}
static gboolean
is_valid_email (const gchar *email)
{
const gchar *at;
const gchar *domain;
at = strchr (email, '@');
if (!at || at == email || strchr (at + 1, '@'))
return FALSE;
domain = at + 1;
if (!strchr (domain, '.') || *domain == '.' || g_str_has_suffix (domain, "."))
return FALSE;
for (const gchar *c = email; *c; c++)
{
if (g_ascii_isspace (*c) || strchr ("<>()[],;:\\\"", *c))
return FALSE;
}
return TRUE;
}
/**
* gcal_util_parse_email_address:
* @text: an address as typed, e.g. "ana@example.com" or "Ana Lima <ana@example.com>"
* @out_email: (out) (optional): return location for the email address
* @out_name: (out) (optional) (nullable): return location for the name, if any
*
* Reads an email address, optionally with a name in front of it.
*
* Returns: %TRUE if @text is an email address
*/
gboolean
gcal_util_parse_email_address (const gchar *text,
gchar **out_email,
gchar **out_name)
{
g_autofree gchar *stripped = NULL;
g_autofree gchar *email = NULL;
g_autofree gchar *name = NULL;
gchar *open_bracket;
if (!text)
return FALSE;
stripped = g_strstrip (g_strdup (text));
open_bracket = strrchr (stripped, '<');
if (open_bracket && g_str_has_suffix (stripped, ">"))
{
stripped[strlen (stripped) - 1] = '\0';
email = g_strstrip (g_strdup (open_bracket + 1));
*open_bracket = '\0';
name = g_strstrip (g_strdup (stripped));
/* "Ana Lima" <ana@example.com> */
if (g_str_has_prefix (name, "\"") && g_str_has_suffix (name, "\"") && strlen (name) >= 2)
{
name[strlen (name) - 1] = '\0';
memmove (name, name + 1, strlen (name));
g_strstrip (name);
}
if (!*name)
g_clear_pointer (&name, g_free);
}
else
{
email = g_strdup (stripped);
}
if (g_ascii_strncasecmp (email, "mailto:", 7) == 0)
memmove (email, email + 7, strlen (email + 7) + 1);
if (!is_valid_email (email))
return FALSE;
if (out_email)
*out_email = g_steal_pointer (&email);
if (out_name)
*out_name = g_steal_pointer (&name);
return TRUE;
}
/**
* gcal_create_activate_signal_shortcuts:
* @widget_class: the class to add the bindings to
+4
View File
@@ -118,6 +118,10 @@ GListModel * gcal_util_create_writable_calendars_model (GcalManager
const gchar * gcal_util_get_email_from_mailto_uri (const gchar *mailto_uri);
gboolean gcal_util_parse_email_address (const gchar *text,
gchar **out_email,
gchar **out_name);
guint gcal_util_create_activate_signal_and_shortcuts (GtkWidgetClass *widget_class,
GType widget_type);
#endif /* __GCAL_UTILS_H__ */