Invite people to events on Federated Core calendars
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
"finish-args" : [
|
"finish-args" : [
|
||||||
"--env=G_DEBUG=fatal-criticals",
|
"--env=G_DEBUG=fatal-criticals",
|
||||||
"--device=dri",
|
"--device=dri",
|
||||||
|
"--filesystem=xdg-config/coredesktop:ro",
|
||||||
"--share=ipc",
|
"--share=ipc",
|
||||||
"--share=network",
|
"--share=network",
|
||||||
"--socket=fallback-x11",
|
"--socket=fallback-x11",
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ assert(cc.has_function('strerror'), '"strerror" not found')
|
|||||||
|
|
||||||
# options
|
# options
|
||||||
config_h.set('ENABLE_TRACING', enable_tracing)
|
config_h.set('ENABLE_TRACING', enable_tracing)
|
||||||
|
config_h.set('ENABLE_FEDERATED_CORE', get_option('federated_core'))
|
||||||
config_h.set('HANDY_USE_UNSTABLE_API', true)
|
config_h.set('HANDY_USE_UNSTABLE_API', true)
|
||||||
|
|
||||||
# _NL_TIME_FIRST_WEEKDAY is an enum and not a define
|
# _NL_TIME_FIRST_WEEKDAY is an enum and not a define
|
||||||
@@ -210,6 +211,9 @@ summary({
|
|||||||
'Source': meson.global_source_root(),
|
'Source': meson.global_source_root(),
|
||||||
'Prefix': calendar_prefix,
|
'Prefix': calendar_prefix,
|
||||||
})
|
})
|
||||||
|
summary({
|
||||||
|
'Federated Core': get_option('federated_core'),
|
||||||
|
}, section: 'Features')
|
||||||
summary({
|
summary({
|
||||||
'Debug': calendar_debug,
|
'Debug': calendar_debug,
|
||||||
'Tracing': enable_tracing,
|
'Tracing': enable_tracing,
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
option('tracing', type: 'boolean', value: false, description: 'add extra debugging information')
|
option('tracing', type: 'boolean', value: false, description: 'add extra debugging information')
|
||||||
option('profile', type: 'combo', choices: ['default', 'development'], value: 'default')
|
option('profile', type: 'combo', choices: ['default', 'development'], value: 'default')
|
||||||
|
option('federated_core', type: 'boolean', value: true, description: 'invite people to events on the calendars of a Federated Computer Core')
|
||||||
|
|||||||
@@ -2023,3 +2023,250 @@ gcal_event_get_organizer (GcalEvent *self)
|
|||||||
|
|
||||||
return self->organizer;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -142,6 +142,26 @@ GListModel * gcal_event_get_attendees (GcalEvent
|
|||||||
|
|
||||||
GcalEventOrganizer * gcal_event_get_organizer (GcalEvent *self);
|
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,
|
gboolean gcal_event_alarms_equal (GcalEvent *event1,
|
||||||
GcalEvent *event2);
|
GcalEvent *event2);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
calendar_incs += include_directories('.')
|
||||||
|
|
||||||
|
sources += files(
|
||||||
|
'gcal-federated-core.c',
|
||||||
|
)
|
||||||
@@ -54,6 +54,8 @@ struct _GcalAttendeeDetailsPage
|
|||||||
GtkWidget *group_not_responded;
|
GtkWidget *group_not_responded;
|
||||||
|
|
||||||
GcalEventAttendeeTypeFilterFlags attendee_type_filter_flags;
|
GcalEventAttendeeTypeFilterFlags attendee_type_filter_flags;
|
||||||
|
|
||||||
|
gboolean removable;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum
|
enum
|
||||||
@@ -64,6 +66,7 @@ enum
|
|||||||
PROP_MODEL_DELEGATED,
|
PROP_MODEL_DELEGATED,
|
||||||
PROP_MODEL_NOT_RESPONDED,
|
PROP_MODEL_NOT_RESPONDED,
|
||||||
PROP_ATTENDEE_TYPE,
|
PROP_ATTENDEE_TYPE,
|
||||||
|
PROP_REMOVABLE,
|
||||||
N_PROPS
|
N_PROPS
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,7 +86,11 @@ static GtkWidget *
|
|||||||
create_attendee_row_widget (void *attendee,
|
create_attendee_row_widget (void *attendee,
|
||||||
void *user_data)
|
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
|
static void
|
||||||
@@ -237,6 +244,9 @@ gcal_attendee_details_page_get_property (GObject *object,
|
|||||||
case PROP_ATTENDEE_TYPE:
|
case PROP_ATTENDEE_TYPE:
|
||||||
g_value_set_flags (value, self->attendee_type_filter_flags);
|
g_value_set_flags (value, self->attendee_type_filter_flags);
|
||||||
break;
|
break;
|
||||||
|
case PROP_REMOVABLE:
|
||||||
|
g_value_set_boolean (value, self->removable);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
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:
|
case PROP_ATTENDEE_TYPE:
|
||||||
set_type_filter_flags (self, g_value_get_flags (value));
|
set_type_filter_flags (self, g_value_get_flags (value));
|
||||||
break;
|
break;
|
||||||
|
case PROP_REMOVABLE:
|
||||||
|
gcal_attendee_details_page_set_removable (self, g_value_get_boolean (value));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
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),
|
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_accepted),
|
||||||
G_LIST_MODEL (instance->filter_list_accepted),
|
G_LIST_MODEL (instance->filter_list_accepted),
|
||||||
create_attendee_row_widget,
|
create_attendee_row_widget,
|
||||||
NULL, NULL);
|
instance, NULL);
|
||||||
|
|
||||||
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_tentative),
|
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_tentative),
|
||||||
G_LIST_MODEL (instance->filter_list_tentative),
|
G_LIST_MODEL (instance->filter_list_tentative),
|
||||||
create_attendee_row_widget,
|
create_attendee_row_widget,
|
||||||
NULL, NULL);
|
instance, NULL);
|
||||||
|
|
||||||
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_declined),
|
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_declined),
|
||||||
G_LIST_MODEL (instance->filter_list_declined),
|
G_LIST_MODEL (instance->filter_list_declined),
|
||||||
create_attendee_row_widget,
|
create_attendee_row_widget,
|
||||||
NULL, NULL);
|
instance, NULL);
|
||||||
|
|
||||||
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_delegated),
|
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_delegated),
|
||||||
G_LIST_MODEL (instance->filter_list_delegated),
|
G_LIST_MODEL (instance->filter_list_delegated),
|
||||||
create_attendee_row_widget,
|
create_attendee_row_widget,
|
||||||
NULL, NULL);
|
instance, NULL);
|
||||||
|
|
||||||
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_not_responded),
|
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_not_responded),
|
||||||
G_LIST_MODEL (instance->filter_list_not_responded),
|
G_LIST_MODEL (instance->filter_list_not_responded),
|
||||||
create_attendee_row_widget,
|
create_attendee_row_widget,
|
||||||
NULL, NULL);
|
instance, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -370,6 +383,16 @@ gcal_attendee_details_page_class_init (GcalAttendeeDetailsPageClass *klass)
|
|||||||
GCAL_EVENT_ATTENDEE_TYPE_FILTER_NONE,
|
GCAL_EVENT_ATTENDEE_TYPE_FILTER_NONE,
|
||||||
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
|
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);
|
g_object_class_install_properties (object_class, N_PROPS, properties);
|
||||||
|
|
||||||
/* widget */
|
/* widget */
|
||||||
@@ -420,3 +443,24 @@ gcal_attendee_details_page_set_type_filter (GcalAttendeeDetailsPage *sel
|
|||||||
g_assert (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
|
g_assert (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
|
||||||
set_type_filter_flags (self, flags);
|
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,
|
void gcal_attendee_details_page_set_type_filter (GcalAttendeeDetailsPage *self,
|
||||||
GcalEventAttendeeTypeFilterFlags flags);
|
GcalEventAttendeeTypeFilterFlags flags);
|
||||||
|
|
||||||
|
void gcal_attendee_details_page_set_removable (GcalAttendeeDetailsPage *self,
|
||||||
|
gboolean removable);
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ template $GcalAttendeeRow: Adw.ActionRow {
|
|||||||
"dimmed",
|
"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 {
|
PopoverMenu context_menu {
|
||||||
|
|||||||
@@ -34,10 +34,13 @@ struct _GcalAttendeeRow
|
|||||||
GcalEventAttendee *attendee;
|
GcalEventAttendee *attendee;
|
||||||
|
|
||||||
GtkWidget *role;
|
GtkWidget *role;
|
||||||
|
GtkWidget *remove_button;
|
||||||
GtkWidget *context_menu;
|
GtkWidget *context_menu;
|
||||||
GtkGestureClick *gesture_click;
|
GtkGestureClick *gesture_click;
|
||||||
|
|
||||||
GSimpleActionGroup *action_group;
|
GSimpleActionGroup *action_group;
|
||||||
|
|
||||||
|
gboolean removable;
|
||||||
};
|
};
|
||||||
|
|
||||||
G_DEFINE_FINAL_TYPE (GcalAttendeeRow, gcal_attendee_row, ADW_TYPE_ACTION_ROW);
|
G_DEFINE_FINAL_TYPE (GcalAttendeeRow, gcal_attendee_row, ADW_TYPE_ACTION_ROW);
|
||||||
@@ -46,6 +49,7 @@ enum
|
|||||||
{
|
{
|
||||||
PROP_0,
|
PROP_0,
|
||||||
PROP_ATTENDEE,
|
PROP_ATTENDEE,
|
||||||
|
PROP_REMOVABLE,
|
||||||
N_PROPS,
|
N_PROPS,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,6 +168,16 @@ gcal_attendee_row_set_property (GObject *object,
|
|||||||
g_assert (self->attendee == NULL);
|
g_assert (self->attendee == NULL);
|
||||||
self->attendee = g_value_get_object (value);
|
self->attendee = g_value_get_object (value);
|
||||||
set_up_row_content (self);
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||||
@@ -183,6 +197,9 @@ gcal_attendee_row_get_property (GObject *object,
|
|||||||
case PROP_ATTENDEE:
|
case PROP_ATTENDEE:
|
||||||
g_value_set_object (value, self->attendee);
|
g_value_set_object (value, self->attendee);
|
||||||
break;
|
break;
|
||||||
|
case PROP_REMOVABLE:
|
||||||
|
g_value_set_boolean (value, self->removable);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
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,
|
GCAL_TYPE_EVENT_ATTENDEE,
|
||||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
|
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);
|
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_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, context_menu);
|
||||||
gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, gesture_click);
|
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);
|
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;
|
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 {
|
$GcalAttendeeSummaryRow {
|
||||||
title: _("Resources");
|
title: _("Resources");
|
||||||
attendees: bind template.attendees;
|
attendees: bind template.attendees;
|
||||||
|
|||||||
@@ -27,17 +27,25 @@
|
|||||||
#include "gcal-event-organizer.h"
|
#include "gcal-event-organizer.h"
|
||||||
#include "gcal-event.h"
|
#include "gcal-event.h"
|
||||||
#include "gcal-organizer-row.h"
|
#include "gcal-organizer-row.h"
|
||||||
|
#include "gcal-utils.h"
|
||||||
#include "glib-object.h"
|
#include "glib-object.h"
|
||||||
|
|
||||||
struct _GcalAttendeesSection
|
struct _GcalAttendeesSection
|
||||||
{
|
{
|
||||||
GcalEventEditorSection parent_instance;
|
GcalEventEditorSection parent_instance;
|
||||||
|
|
||||||
|
GcalEvent *event;
|
||||||
GcalEventOrganizer *organizer;
|
GcalEventOrganizer *organizer;
|
||||||
|
|
||||||
|
GtkWidget *invite_row;
|
||||||
|
GtkWidget *organizer_row;
|
||||||
AdwActionRow *summary_row;
|
AdwActionRow *summary_row;
|
||||||
|
|
||||||
GListModel *attendees;
|
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)
|
G_DEFINE_FINAL_TYPE (GcalAttendeesSection, gcal_attendees_section, GCAL_TYPE_EVENT_EDITOR_SECTION)
|
||||||
@@ -46,6 +54,7 @@ enum
|
|||||||
{
|
{
|
||||||
PROP_0,
|
PROP_0,
|
||||||
PROP_ATTENDEES,
|
PROP_ATTENDEES,
|
||||||
|
PROP_CAN_INVITE,
|
||||||
PROP_ORGANIZER,
|
PROP_ORGANIZER,
|
||||||
N_PROPS
|
N_PROPS
|
||||||
};
|
};
|
||||||
@@ -53,6 +62,97 @@ enum
|
|||||||
static GParamSpec *properties[N_PROPS] = { NULL, };
|
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
|
* GcalEventEditorSection overrides
|
||||||
*/
|
*/
|
||||||
@@ -63,19 +163,23 @@ gcal_attendees_section_event_set_cb (GcalEventEditorSection *section,
|
|||||||
{
|
{
|
||||||
GcalAttendeesSection *self = GCAL_ATTENDEES_SECTION (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)
|
if (event)
|
||||||
{
|
{
|
||||||
self->organizer = gcal_event_get_organizer (event);
|
g_signal_connect_object (gcal_event_get_attendees (event),
|
||||||
self->attendees = gcal_event_get_attendees (event);
|
"items-changed",
|
||||||
}
|
G_CALLBACK (on_attendees_changed_cb),
|
||||||
else
|
self,
|
||||||
{
|
G_CONNECT_SWAPPED);
|
||||||
self->organizer = NULL;
|
|
||||||
self->attendees = NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ATTENDEES]);
|
update_from_event (self);
|
||||||
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ORGANIZER]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -97,6 +201,10 @@ gcal_attendees_section_get_property (GObject *object,
|
|||||||
g_value_set_object (value, self->attendees);
|
g_value_set_object (value, self->attendees);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case PROP_CAN_INVITE:
|
||||||
|
g_value_set_boolean (value, self->inviter_email != NULL);
|
||||||
|
break;
|
||||||
|
|
||||||
case PROP_ORGANIZER:
|
case PROP_ORGANIZER:
|
||||||
g_value_set_object (value, self->organizer);
|
g_value_set_object (value, self->organizer);
|
||||||
break;
|
break;
|
||||||
@@ -132,6 +240,13 @@ gcal_attendees_section_finalize (GObject *object)
|
|||||||
|
|
||||||
self->attendees = NULL; /* not owned */
|
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);
|
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_TYPE_LIST_MODEL,
|
||||||
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
|
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:
|
* 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_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_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 ())
|
#define GCAL_TYPE_ATTENDEES_SECTION (gcal_attendees_section_get_type ())
|
||||||
G_DECLARE_FINAL_TYPE (GcalAttendeesSection, gcal_attendees_section, GCAL, ATTENDEES_SECTION, GcalEventEditorSection)
|
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
|
G_END_DECLS
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "gcal-event-editor-dialog.h"
|
#include "gcal-event-editor-dialog.h"
|
||||||
#include "gcal-event-editor-section.h"
|
#include "gcal-event-editor-section.h"
|
||||||
#include "gcal-event.h"
|
#include "gcal-event.h"
|
||||||
|
#include "gcal-federated-core.h"
|
||||||
#include "gcal-notes-section.h"
|
#include "gcal-notes-section.h"
|
||||||
#include "gcal-reminders-section.h"
|
#include "gcal-reminders-section.h"
|
||||||
#include "gcal-schedule-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]);
|
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
|
static void
|
||||||
set_event (GcalEventEditorDialog *self,
|
set_event (GcalEventEditorDialog *self,
|
||||||
GcalEvent *event,
|
GcalEvent *event,
|
||||||
@@ -165,8 +208,7 @@ set_event (GcalEventEditorDialog *self,
|
|||||||
gcal_calendar_combo_row_set_calendar (self->calendar_combo_row, calendar);
|
gcal_calendar_combo_row_set_calendar (self->calendar_combo_row, calendar);
|
||||||
|
|
||||||
set_writable (self, !gcal_calendar_is_read_only (calendar));
|
set_writable (self, !gcal_calendar_is_read_only (calendar));
|
||||||
gboolean has_participants = gcal_event_get_organizer (self->edited_event) != NULL;
|
update_invite_state (self);
|
||||||
gtk_widget_set_visible (GTK_WIDGET (self->attendees_section), has_participants);
|
|
||||||
|
|
||||||
gtk_widget_set_visible (self->cancel_button, self->writable);
|
gtk_widget_set_visible (self->cancel_button, self->writable);
|
||||||
|
|
||||||
@@ -235,6 +277,9 @@ apply_event_properties_to_template_event (GcalEvent *template_event,
|
|||||||
if (alarm)
|
if (alarm)
|
||||||
gcal_event_add_alarm (template_event, 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
|
static void
|
||||||
@@ -275,7 +320,7 @@ save_event (GcalEventEditorDialog *self,
|
|||||||
GcalContext *context;
|
GcalContext *context;
|
||||||
GcalManager *manager;
|
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;
|
GCAL_ENTRY;
|
||||||
|
|
||||||
@@ -284,11 +329,20 @@ save_event (GcalEventEditorDialog *self,
|
|||||||
|
|
||||||
selected_calendar = gcal_calendar_combo_row_get_calendar (self->calendar_combo_row);
|
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;
|
calendar_equal = selected_calendar && calendar == selected_calendar;
|
||||||
alarms_equal = gcal_event_alarms_equal (self->event, self->edited_event);
|
alarms_equal = gcal_event_alarms_equal (self->event, self->edited_event);
|
||||||
schedule_equal = gcal_event_schedule_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);
|
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)
|
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));
|
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
|
static void
|
||||||
on_cancel_button_clicked_cb (GtkButton *button,
|
on_cancel_button_clicked_cb (GtkButton *button,
|
||||||
GcalEventEditorDialog *self)
|
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.save", NULL, on_event_editor_save_action_activated_cb);
|
||||||
gtk_widget_class_install_action (widget_class, "event-editor.show-attendees-detail-page", "u",
|
gtk_widget_class_install_action (widget_class, "event-editor.show-attendees-detail-page", "u",
|
||||||
on_show_attendees_detail_page_action_activated_cb);
|
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
|
static void
|
||||||
@@ -648,6 +723,7 @@ gcal_event_editor_dialog_init (GcalEventEditorDialog *self)
|
|||||||
set_up_context (self);
|
set_up_context (self);
|
||||||
|
|
||||||
g_signal_connect_swapped (self, "validation-requested", G_CALLBACK (on_validation_requested_cb), 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;
|
return NULL;
|
||||||
|
|
||||||
email = gcal_util_get_email_from_mailto_uri (gcal_event_organizer_get_uri (organizer));
|
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);
|
name_with_email = g_strdup_printf ("%s (%s)", gcal_event_organizer_get_name (organizer), email);
|
||||||
|
|
||||||
return g_strdup (name_with_email);
|
return g_strdup (name_with_email);
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ sources = files()
|
|||||||
subdir('appdata')
|
subdir('appdata')
|
||||||
subdir('contrib')
|
subdir('contrib')
|
||||||
subdir('core')
|
subdir('core')
|
||||||
|
subdir('federated')
|
||||||
subdir('gui')
|
subdir('gui')
|
||||||
subdir('search')
|
subdir('search')
|
||||||
subdir('theme')
|
subdir('theme')
|
||||||
|
|||||||
@@ -1081,6 +1081,93 @@ gcal_util_get_email_from_mailto_uri (const gchar *mailto_uri)
|
|||||||
return address;
|
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:
|
* gcal_create_activate_signal_shortcuts:
|
||||||
* @widget_class: the class to add the bindings to
|
* @widget_class: the class to add the bindings to
|
||||||
|
|||||||
@@ -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);
|
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,
|
guint gcal_util_create_activate_signal_and_shortcuts (GtkWidgetClass *widget_class,
|
||||||
GType widget_type);
|
GType widget_type);
|
||||||
#endif /* __GCAL_UTILS_H__ */
|
#endif /* __GCAL_UTILS_H__ */
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ tests = [
|
|||||||
'server',
|
'server',
|
||||||
'utils',
|
'utils',
|
||||||
'event-attendee',
|
'event-attendee',
|
||||||
|
'federated-core',
|
||||||
]
|
]
|
||||||
|
|
||||||
foreach test : tests
|
foreach test : tests
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#include "gcal-event-attendee.h"
|
#include "gcal-event-attendee.h"
|
||||||
#include "gcal-event-organizer.h"
|
#include "gcal-event-organizer.h"
|
||||||
@@ -454,6 +455,97 @@ event_get_attendees (void)
|
|||||||
|
|
||||||
/*********************************************************************************************************************/
|
/*********************************************************************************************************************/
|
||||||
|
|
||||||
|
static void
|
||||||
|
event_invite_attendees (void)
|
||||||
|
{
|
||||||
|
g_autoptr (GcalEvent) event = NULL;
|
||||||
|
g_autoptr (GcalEvent) clone = NULL;
|
||||||
|
g_autoptr (GError) error = NULL;
|
||||||
|
g_autofree gchar *ical = NULL;
|
||||||
|
GcalEventOrganizer *organizer;
|
||||||
|
GListModel *attendees;
|
||||||
|
|
||||||
|
event = create_event_for_string (STUB_EVENT, &error);
|
||||||
|
g_assert_no_error (error);
|
||||||
|
|
||||||
|
attendees = gcal_event_get_attendees (event);
|
||||||
|
g_assert_cmpuint (g_list_model_get_n_items (attendees), ==, 0);
|
||||||
|
g_assert_null (gcal_event_get_organizer (event));
|
||||||
|
|
||||||
|
clone = gcal_event_new_from_event (event);
|
||||||
|
g_assert_true (gcal_event_attendees_equal (event, clone));
|
||||||
|
|
||||||
|
gcal_event_set_organizer (clone, "me@example.com", "Me Myself");
|
||||||
|
organizer = gcal_event_get_organizer (clone);
|
||||||
|
g_assert_nonnull (organizer);
|
||||||
|
g_assert_cmpstr (gcal_event_organizer_get_uri (organizer), ==, "mailto:me@example.com");
|
||||||
|
g_assert_cmpstr (gcal_event_organizer_get_name (organizer), ==, "Me Myself");
|
||||||
|
g_assert_false (gcal_event_attendees_equal (event, clone));
|
||||||
|
|
||||||
|
g_assert_true (gcal_event_add_attendee (clone, "ana@example.com", "Ana Lima"));
|
||||||
|
g_assert_true (gcal_event_add_attendee (clone, "bo@example.com", NULL));
|
||||||
|
g_assert_false (gcal_event_add_attendee (clone, "ANA@example.com", NULL));
|
||||||
|
|
||||||
|
attendees = gcal_event_get_attendees (clone);
|
||||||
|
g_assert_cmpuint (g_list_model_get_n_items (attendees), ==, 2);
|
||||||
|
g_assert_true (gcal_event_has_attendee (clone, "mailto:Ana@Example.com"));
|
||||||
|
g_assert_false (gcal_event_has_attendee (clone, "carl@example.com"));
|
||||||
|
|
||||||
|
{
|
||||||
|
g_autoptr (GcalEventAttendee) attendee = g_list_model_get_item (attendees, 0);
|
||||||
|
|
||||||
|
g_assert_cmpstr (gcal_event_attendee_get_uri (attendee), ==, "mailto:ana@example.com");
|
||||||
|
g_assert_cmpstr (gcal_event_attendee_get_name (attendee), ==, "Ana Lima");
|
||||||
|
g_assert_cmpint (gcal_event_attendee_get_attendee_type (attendee), ==, GCAL_EVENT_ATTENDEE_TYPE_INDIVIDUAL);
|
||||||
|
g_assert_cmpint (gcal_event_attendee_get_role (attendee), ==, GCAL_EVENT_ATTENDEE_ROLE_REQUIRED);
|
||||||
|
g_assert_cmpint (gcal_event_attendee_get_part_status (attendee), ==, GCAL_EVENT_ATTENDEE_PART_NEEDS_ACTION);
|
||||||
|
g_assert_true (gcal_event_attendee_get_requires_rsvp (attendee));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* What the server sees */
|
||||||
|
ical = e_cal_component_get_as_string (gcal_event_get_component (clone));
|
||||||
|
g_assert_nonnull (strstr (ical, "ORGANIZER;CN=Me Myself:mailto:me@example.com"));
|
||||||
|
g_assert_nonnull (strstr (ical, "mailto:ana@example.com"));
|
||||||
|
g_assert_nonnull (strstr (ical, "mailto:bo@example.com"));
|
||||||
|
|
||||||
|
gcal_event_remove_attendee (clone, "mailto:ANA@example.com");
|
||||||
|
g_assert_cmpuint (g_list_model_get_n_items (attendees), ==, 1);
|
||||||
|
g_assert_false (gcal_event_has_attendee (clone, "ana@example.com"));
|
||||||
|
|
||||||
|
/* Removing someone who isn't invited does nothing */
|
||||||
|
gcal_event_remove_attendee (clone, "carl@example.com");
|
||||||
|
g_assert_cmpuint (g_list_model_get_n_items (attendees), ==, 1);
|
||||||
|
|
||||||
|
gcal_event_remove_attendee (clone, "bo@example.com");
|
||||||
|
gcal_event_set_organizer (clone, NULL, NULL);
|
||||||
|
g_assert_null (gcal_event_get_organizer (clone));
|
||||||
|
g_assert_true (gcal_event_attendees_equal (event, clone));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
event_copy_attendees (void)
|
||||||
|
{
|
||||||
|
g_autoptr (GcalEvent) with_attendees = NULL;
|
||||||
|
g_autoptr (GcalEvent) event = NULL;
|
||||||
|
g_autoptr (GError) error = NULL;
|
||||||
|
|
||||||
|
with_attendees = create_event_for_string (STUB_EVENT_WITH_ATTENDEES, &error);
|
||||||
|
g_assert_no_error (error);
|
||||||
|
|
||||||
|
event = create_event_for_string (STUB_EVENT, &error);
|
||||||
|
g_assert_no_error (error);
|
||||||
|
|
||||||
|
g_assert_false (gcal_event_attendees_equal (event, with_attendees));
|
||||||
|
|
||||||
|
gcal_event_copy_attendees (event, with_attendees);
|
||||||
|
|
||||||
|
g_assert_true (gcal_event_attendees_equal (event, with_attendees));
|
||||||
|
g_assert_cmpuint (g_list_model_get_n_items (gcal_event_get_attendees (event)), ==, 2);
|
||||||
|
g_assert_cmpstr (gcal_event_organizer_get_uri (gcal_event_get_organizer (event)), ==, "mailto:alice@example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************************************/
|
||||||
|
|
||||||
static GDateTime *
|
static GDateTime *
|
||||||
dt_add_second_random (GDateTime *dt)
|
dt_add_second_random (GDateTime *dt)
|
||||||
{
|
{
|
||||||
@@ -595,6 +687,8 @@ main (gint argc,
|
|||||||
g_test_add_func ("/event/date/create-tzid", event_date_create_tzid);
|
g_test_add_func ("/event/date/create-tzid", event_date_create_tzid);
|
||||||
g_test_add_func ("/event/date/check-tz", event_date_check_tz);
|
g_test_add_func ("/event/date/check-tz", event_date_check_tz);
|
||||||
g_test_add_func ("/event/date/get-attendees", event_get_attendees);
|
g_test_add_func ("/event/date/get-attendees", event_get_attendees);
|
||||||
|
g_test_add_func ("/event/attendees/invite", event_invite_attendees);
|
||||||
|
g_test_add_func ("/event/attendees/copy", event_copy_attendees);
|
||||||
g_test_add_func ("/event/equal/schedule", event_schedule_equal);
|
g_test_add_func ("/event/equal/schedule", event_schedule_equal);
|
||||||
|
|
||||||
return g_test_run ();
|
return g_test_run ();
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/* test-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
|
||||||
|
#include "gcal-federated-core.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Written by CoreDesktop's configure with bash's printf %q
|
||||||
|
*/
|
||||||
|
#define CORE_CONF(domain, email, name) \
|
||||||
|
"CORE_DOMAIN=" domain "\n" \
|
||||||
|
"CORE_EMAIL=" email "\n" \
|
||||||
|
"CORE_NAME=" name "\n"
|
||||||
|
|
||||||
|
static gchar *
|
||||||
|
create_config_dir (void)
|
||||||
|
{
|
||||||
|
g_autoptr (GError) error = NULL;
|
||||||
|
gchar *config_dir;
|
||||||
|
|
||||||
|
config_dir = g_dir_make_tmp ("gcal-federated-core-XXXXXX", &error);
|
||||||
|
g_assert_no_error (error);
|
||||||
|
|
||||||
|
return config_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
write_file (const gchar *config_dir,
|
||||||
|
const gchar *filename,
|
||||||
|
const gchar *contents)
|
||||||
|
{
|
||||||
|
g_autoptr (GError) error = NULL;
|
||||||
|
g_autofree gchar *path = g_build_filename (config_dir, filename, NULL);
|
||||||
|
g_autofree gchar *dir = g_path_get_dirname (path);
|
||||||
|
|
||||||
|
g_mkdir_with_parents (dir, 0700);
|
||||||
|
g_file_set_contents (path, contents, -1, &error);
|
||||||
|
g_assert_no_error (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
remove_config_dir (const gchar *config_dir)
|
||||||
|
{
|
||||||
|
g_autoptr (GFile) dir = g_file_new_for_path (config_dir);
|
||||||
|
g_autoptr (GFileEnumerator) children = NULL;
|
||||||
|
GFileInfo *info;
|
||||||
|
GFile *child;
|
||||||
|
|
||||||
|
children = g_file_enumerate_children (dir, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
|
||||||
|
|
||||||
|
while (children && g_file_enumerator_iterate (children, &info, &child, NULL, NULL) && info)
|
||||||
|
{
|
||||||
|
if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
|
||||||
|
{
|
||||||
|
g_autofree gchar *path = g_file_get_path (child);
|
||||||
|
remove_config_dir (path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g_file_delete (child, NULL, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_file_delete (dir, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************************************/
|
||||||
|
|
||||||
|
static void
|
||||||
|
federated_core_find_core (void)
|
||||||
|
{
|
||||||
|
g_autofree gchar *config_dir = create_config_dir ();
|
||||||
|
g_autofree gchar *email = NULL;
|
||||||
|
g_autofree gchar *name = NULL;
|
||||||
|
|
||||||
|
/* Nothing set up */
|
||||||
|
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.com", NULL, NULL));
|
||||||
|
|
||||||
|
write_file (config_dir, "cores/example.com.conf", CORE_CONF ("example.com", "ana@example.com", "Ana\\ Lima"));
|
||||||
|
write_file (config_dir, "cores/example.org.conf", CORE_CONF ("example.org", "bo@example.org", "Bo"));
|
||||||
|
|
||||||
|
g_assert_true (gcal_federated_core_find_core (config_dir, "nextcloud.example.com", &email, &name));
|
||||||
|
g_assert_cmpstr (email, ==, "ana@example.com");
|
||||||
|
g_assert_cmpstr (name, ==, "Ana Lima");
|
||||||
|
|
||||||
|
g_clear_pointer (&email, g_free);
|
||||||
|
g_clear_pointer (&name, g_free);
|
||||||
|
|
||||||
|
g_assert_true (gcal_federated_core_find_core (config_dir, "NextCloud.Example.Org", &email, &name));
|
||||||
|
g_assert_cmpstr (email, ==, "bo@example.org");
|
||||||
|
g_assert_cmpstr (name, ==, "Bo");
|
||||||
|
|
||||||
|
/* Only the Core's own Nextcloud */
|
||||||
|
g_assert_false (gcal_federated_core_find_core (config_dir, "example.com", NULL, NULL));
|
||||||
|
g_assert_false (gcal_federated_core_find_core (config_dir, "caldav.example.com", NULL, NULL));
|
||||||
|
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.com.evil.net", NULL, NULL));
|
||||||
|
g_assert_false (gcal_federated_core_find_core (config_dir, NULL, NULL, NULL));
|
||||||
|
|
||||||
|
remove_config_dir (config_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
federated_core_single_core_conf (void)
|
||||||
|
{
|
||||||
|
g_autofree gchar *config_dir = create_config_dir ();
|
||||||
|
g_autofree gchar *email = NULL;
|
||||||
|
g_autofree gchar *name = NULL;
|
||||||
|
|
||||||
|
/* Before CoreDesktop supported several Cores */
|
||||||
|
write_file (config_dir, "core.conf", CORE_CONF ("example.com", "ana@example.com", "\\'Ana\\'"));
|
||||||
|
|
||||||
|
g_assert_true (gcal_federated_core_find_core (config_dir, "nextcloud.example.com", &email, &name));
|
||||||
|
g_assert_cmpstr (email, ==, "ana@example.com");
|
||||||
|
g_assert_cmpstr (name, ==, "'Ana'");
|
||||||
|
|
||||||
|
remove_config_dir (config_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
federated_core_incomplete_conf (void)
|
||||||
|
{
|
||||||
|
g_autofree gchar *config_dir = create_config_dir ();
|
||||||
|
|
||||||
|
write_file (config_dir, "cores/example.com.conf", "CORE_DOMAIN=example.com\n");
|
||||||
|
write_file (config_dir, "cores/example.org.conf", CORE_CONF ("example.org", "not-an-email", "Bo"));
|
||||||
|
write_file (config_dir, "cores/example.net.conf.bak", CORE_CONF ("example.net", "cy@example.net", "Cy"));
|
||||||
|
|
||||||
|
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.com", NULL, NULL));
|
||||||
|
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.org", NULL, NULL));
|
||||||
|
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.net", NULL, NULL));
|
||||||
|
|
||||||
|
remove_config_dir (config_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************************************/
|
||||||
|
|
||||||
|
gint
|
||||||
|
main (gint argc,
|
||||||
|
gchar *argv[])
|
||||||
|
{
|
||||||
|
g_test_init (&argc, &argv, NULL);
|
||||||
|
|
||||||
|
g_test_add_func ("/federated-core/find-core", federated_core_find_core);
|
||||||
|
g_test_add_func ("/federated-core/single-core-conf", federated_core_single_core_conf);
|
||||||
|
g_test_add_func ("/federated-core/incomplete-conf", federated_core_incomplete_conf);
|
||||||
|
|
||||||
|
return g_test_run ();
|
||||||
|
}
|
||||||
@@ -233,6 +233,52 @@ test_first_weekday (void)
|
|||||||
|
|
||||||
/*********************************************************************************************************************/
|
/*********************************************************************************************************************/
|
||||||
|
|
||||||
|
static void
|
||||||
|
parse_email_address (void)
|
||||||
|
{
|
||||||
|
struct {
|
||||||
|
const gchar *text;
|
||||||
|
const gchar *email;
|
||||||
|
const gchar *name;
|
||||||
|
} valid[] = {
|
||||||
|
{ "ana@example.com", "ana@example.com", NULL },
|
||||||
|
{ " ana@example.com ", "ana@example.com", NULL },
|
||||||
|
{ "mailto:ana@example.com", "ana@example.com", NULL },
|
||||||
|
{ "Ana Lima <ana@example.com>", "ana@example.com", "Ana Lima" },
|
||||||
|
{ "\"Lima, Ana\" <ana@example.com>", "ana@example.com", "Lima, Ana" },
|
||||||
|
{ "<ana@example.com>", "ana@example.com", NULL },
|
||||||
|
{ "ana.lima+work@mail.example.co.uk", "ana.lima+work@mail.example.co.uk", NULL },
|
||||||
|
};
|
||||||
|
const gchar *invalid[] = {
|
||||||
|
NULL,
|
||||||
|
"",
|
||||||
|
"ana",
|
||||||
|
"ana@",
|
||||||
|
"@example.com",
|
||||||
|
"ana@example",
|
||||||
|
"ana@example.",
|
||||||
|
"ana@.example.com",
|
||||||
|
"ana@@example.com",
|
||||||
|
"ana lima@example.com",
|
||||||
|
"Ana Lima <ana>",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (gsize i = 0; i < G_N_ELEMENTS (valid); i++)
|
||||||
|
{
|
||||||
|
g_autofree gchar *email = NULL;
|
||||||
|
g_autofree gchar *name = NULL;
|
||||||
|
|
||||||
|
g_assert_true (gcal_util_parse_email_address (valid[i].text, &email, &name));
|
||||||
|
g_assert_cmpstr (email, ==, valid[i].email);
|
||||||
|
g_assert_cmpstr (name, ==, valid[i].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (gsize i = 0; i < G_N_ELEMENTS (invalid); i++)
|
||||||
|
g_assert_false (gcal_util_parse_email_address (invalid[i], NULL, NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************************************/
|
||||||
|
|
||||||
gint
|
gint
|
||||||
main (gint argc,
|
main (gint argc,
|
||||||
gchar *argv[])
|
gchar *argv[])
|
||||||
@@ -245,6 +291,7 @@ main (gint argc,
|
|||||||
g_test_add_func ("/utils/date-time/date_time_from_icaltime", date_time_from_icaltime);
|
g_test_add_func ("/utils/date-time/date_time_from_icaltime", date_time_from_icaltime);
|
||||||
g_test_add_func ("/utils/misc/extract_meeting_url", extract_meeting_url);
|
g_test_add_func ("/utils/misc/extract_meeting_url", extract_meeting_url);
|
||||||
g_test_add_func ("/utils/misc/get_first_weekday", test_first_weekday);
|
g_test_add_func ("/utils/misc/get_first_weekday", test_first_weekday);
|
||||||
|
g_test_add_func ("/utils/misc/parse_email_address", parse_email_address);
|
||||||
|
|
||||||
return g_test_run ();
|
return g_test_run ();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user