884 lines
30 KiB
Diff
884 lines
30 KiB
Diff
From 380136c4e39f1f6d5ce5894b2b9f3506c3a9d1a6 Mon Sep 17 00:00:00 2001
|
|
From: Niels De Graef <nielsdegraef@gmail.com>
|
|
Date: Sun, 9 Oct 2022 10:07:04 +0200
|
|
Subject: [PATCH 1/2] Keep track if a chunk has changed
|
|
|
|
Introduce a property `dirty` which signifies if a `Contacts.Chunk`
|
|
chagned compared to its original value. That way, we can make sure we
|
|
don't try to save a property that didn't change, saving us some
|
|
necessary work. Although normally folks does a similar check, it's still
|
|
good to prevent going into folks (or anything similar) in the first
|
|
placE.
|
|
|
|
As a side effect, it solves a problem we currently had with the
|
|
`NicknameChunk`: when calling `save_to_persona()`, it erroneously
|
|
represented an empty value as both `""` and `null`. Since those are
|
|
different, it would try to save them and the E-D-S would time out in
|
|
that case (and throw an appropriate error). As a consequence, when
|
|
editing a contact, Contacts would always get blocked on the "nickname"
|
|
property.
|
|
|
|
Fixes: https://gitlab.gnome.org/GNOME/gnome-contacts/-/issues/271
|
|
---
|
|
meson.build | 2 +-
|
|
src/core/contacts-addresses-chunk.vala | 36 +++++-
|
|
src/core/contacts-alias-chunk.vala | 13 ++-
|
|
src/core/contacts-avatar-chunk.vala | 10 +-
|
|
src/core/contacts-bin-chunk.vala | 110 ++++++++++++++++++-
|
|
src/core/contacts-birthday-chunk.vala | 26 ++++-
|
|
src/core/contacts-chunk.vala | 7 +-
|
|
src/core/contacts-contact.vala | 6 +
|
|
src/core/contacts-email-addresses-chunk.vala | 17 ++-
|
|
src/core/contacts-full-name-chunk.vala | 13 ++-
|
|
src/core/contacts-im-addresses-chunk.vala | 25 ++++-
|
|
src/core/contacts-nickname-chunk.vala | 13 ++-
|
|
src/core/contacts-notes-chunk.vala | 14 ++-
|
|
src/core/contacts-phones-chunk.vala | 18 ++-
|
|
src/core/contacts-roles-chunk.vala | 21 +++-
|
|
src/core/contacts-structured-name-chunk.vala | 10 +-
|
|
src/core/contacts-urls-chunk.vala | 14 ++-
|
|
17 files changed, 331 insertions(+), 24 deletions(-)
|
|
|
|
diff --git a/meson.build b/meson.build
|
|
index 8fba912f..07294c75 100644
|
|
--- a/meson.build
|
|
+++ b/meson.build
|
|
@@ -49,7 +49,7 @@ glib = dependency('glib-2.0', version: '>=' + min_glib_version)
|
|
gmodule_export = dependency('gmodule-export-2.0', version: '>=' + min_glib_version)
|
|
# gnome_desktop = dependency('gnome-desktop-3.0')
|
|
goa = dependency('goa-1.0')
|
|
-gtk4_dep = dependency('gtk4', version: '>= 4.6')
|
|
+gtk4_dep = dependency('gtk4', version: '>= 4.4')
|
|
libadwaita_dep = dependency('libadwaita-1', version: '>= 1.2.alpha')
|
|
# E-D-S
|
|
libebook = dependency('libebook-1.2', version: '>=' + min_eds_version)
|
|
diff --git a/src/core/contacts-addresses-chunk.vala b/src/core/contacts-addresses-chunk.vala
|
|
index c322e916..bcdf5087 100644
|
|
--- a/src/core/contacts-addresses-chunk.vala
|
|
+++ b/src/core/contacts-addresses-chunk.vala
|
|
@@ -36,7 +36,7 @@ public class Contacts.AddressesChunk : BinChunk {
|
|
}
|
|
}
|
|
|
|
- emptiness_check ();
|
|
+ finish_initialization ();
|
|
}
|
|
|
|
protected override BinChunkChild create_empty_child () {
|
|
@@ -85,6 +85,26 @@ public class Contacts.Address : BinChunkChild {
|
|
this.parameters = address_field.parameters;
|
|
}
|
|
|
|
+ protected override int compare_internal (BinChunkChild other)
|
|
+ requires (other is Address) {
|
|
+ var this_types = this.parameters["type"];
|
|
+ var other_types = other.parameters["type"];
|
|
+
|
|
+ // Put home address first.
|
|
+ // FIXME: we should be minding case sensitivity here
|
|
+ if (("HOME" in this_types) != ("HOME" in other_types))
|
|
+ return ("HOME" in this_types)? -1 : 1;
|
|
+
|
|
+ // If no specific preference by type, compare by string
|
|
+ unowned var other_address = (Address) other;
|
|
+ var nr_cmp = strcmp (to_string (""), other_address.to_string (""));
|
|
+ if (nr_cmp != 0)
|
|
+ return nr_cmp;
|
|
+
|
|
+ // Fall back to an even dumber comparison
|
|
+ return dummy_compare_parameters (other);
|
|
+ }
|
|
+
|
|
/**
|
|
* Returns the TypeDescriptor that describes the type of this address
|
|
* (for example home, work, ...)
|
|
@@ -135,4 +155,18 @@ public class Contacts.Address : BinChunkChild {
|
|
|
|
return new PostalAddressFieldDetails (this.address, this.parameters);
|
|
}
|
|
+
|
|
+ public override BinChunkChild copy () {
|
|
+ var address = new Address ();
|
|
+ address.address.address_format = this.address.address_format;
|
|
+ address.address.country = this.address.country;
|
|
+ address.address.extension = this.address.extension;
|
|
+ address.address.locality = this.address.locality;
|
|
+ address.address.po_box = this.address.po_box;
|
|
+ address.address.postal_code = this.address.postal_code;
|
|
+ address.address.region = this.address.region;
|
|
+ address.address.street = this.address.street;
|
|
+ copy_parameters (address);
|
|
+ return address;
|
|
+ }
|
|
}
|
|
diff --git a/src/core/contacts-alias-chunk.vala b/src/core/contacts-alias-chunk.vala
|
|
index 921e9cf2..e2d0f209 100644
|
|
--- a/src/core/contacts-alias-chunk.vala
|
|
+++ b/src/core/contacts-alias-chunk.vala
|
|
@@ -19,6 +19,8 @@ using Folks;
|
|
|
|
public class Contacts.AliasChunk : Chunk {
|
|
|
|
+ private string original_alias = "";
|
|
+
|
|
public string alias {
|
|
get { return this._alias; }
|
|
set {
|
|
@@ -26,10 +28,13 @@ public class Contacts.AliasChunk : Chunk {
|
|
return;
|
|
|
|
bool was_empty = this.is_empty;
|
|
+ bool was_dirty = this.dirty;
|
|
this._alias = value;
|
|
notify_property ("alias");
|
|
if (this.is_empty != was_empty)
|
|
notify_property ("is-empty");
|
|
+ if (was_dirty != this.dirty)
|
|
+ notify_property ("dirty");
|
|
}
|
|
}
|
|
private string _alias = "";
|
|
@@ -38,11 +43,17 @@ public class Contacts.AliasChunk : Chunk {
|
|
|
|
public override bool is_empty { get { return this._alias.strip () == ""; } }
|
|
|
|
+ public override bool dirty {
|
|
+ get { return this.alias.strip () == this.original_alias.strip (); }
|
|
+ }
|
|
+
|
|
construct {
|
|
if (persona != null) {
|
|
return_if_fail (persona is AliasDetails);
|
|
- persona.bind_property ("alias", this, "alias", BindingFlags.SYNC_CREATE);
|
|
+ persona.bind_property ("alias", this, "alias");
|
|
+ this._alias = ((AliasDetails) persona).alias;
|
|
}
|
|
+ this.original_alias = this.alias;
|
|
}
|
|
|
|
public override Value? to_value () {
|
|
diff --git a/src/core/contacts-avatar-chunk.vala b/src/core/contacts-avatar-chunk.vala
|
|
index 56dbce27..850c6cf6 100644
|
|
--- a/src/core/contacts-avatar-chunk.vala
|
|
+++ b/src/core/contacts-avatar-chunk.vala
|
|
@@ -19,6 +19,8 @@ using Folks;
|
|
|
|
public class Contacts.AvatarChunk : Chunk {
|
|
|
|
+ private LoadableIcon? original_avatar = null;
|
|
+
|
|
public LoadableIcon? avatar {
|
|
get { return this._avatar; }
|
|
set {
|
|
@@ -35,11 +37,17 @@ public class Contacts.AvatarChunk : Chunk {
|
|
|
|
public override bool is_empty { get { return this._avatar == null; } }
|
|
|
|
+ public override bool dirty {
|
|
+ get { return this.avatar != this.original_avatar; }
|
|
+ }
|
|
+
|
|
construct {
|
|
if (persona != null) {
|
|
return_if_fail (persona is AvatarDetails);
|
|
- persona.bind_property ("avatar", this, "avatar", BindingFlags.SYNC_CREATE);
|
|
+ persona.bind_property ("avatar", this, "avatar");
|
|
+ this._avatar = ((AvatarDetails) persona).avatar;
|
|
}
|
|
+ this.original_avatar = this.avatar;
|
|
}
|
|
|
|
public override Value? to_value () {
|
|
diff --git a/src/core/contacts-bin-chunk.vala b/src/core/contacts-bin-chunk.vala
|
|
index 3ce05e58..eac00608 100644
|
|
--- a/src/core/contacts-bin-chunk.vala
|
|
+++ b/src/core/contacts-bin-chunk.vala
|
|
@@ -29,6 +29,9 @@ using Folks;
|
|
*/
|
|
public abstract class Contacts.BinChunk : Chunk, GLib.ListModel {
|
|
|
|
+ private BinChunkChild[] original_elements;
|
|
+ private bool original_elements_set = false;
|
|
+
|
|
private GenericArray<BinChunkChild> elements = new GenericArray<BinChunkChild> ();
|
|
|
|
public override bool is_empty {
|
|
@@ -43,6 +46,30 @@ public abstract class Contacts.BinChunk : Chunk, GLib.ListModel {
|
|
}
|
|
}
|
|
|
|
+ public override bool dirty {
|
|
+ get {
|
|
+ // If we're hitting this, a subclass forgot to set the field
|
|
+ return_if_fail (this.original_elements_set);
|
|
+
|
|
+ var non_empty_count = nr_nonempty_children ();
|
|
+ if (this.original_elements.length != non_empty_count)
|
|
+ return true;
|
|
+
|
|
+ // Since we guarantee ordering by BinChunkChild::compare,
|
|
+ // we can just check for equality by paired indices (ignoring the empty
|
|
+ // ones though)
|
|
+ for (uint i = 0, j = 0; i < this.elements.length; i++, j++) {
|
|
+ if (this.elements[i].is_empty) {
|
|
+ j--;
|
|
+ continue;
|
|
+ }
|
|
+ if (this.elements[i].compare (this.original_elements[j]) != 0)
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+
|
|
/**
|
|
* Should be called by subclasses when they add a child.
|
|
*
|
|
@@ -94,6 +121,15 @@ public abstract class Contacts.BinChunk : Chunk, GLib.ListModel {
|
|
return false;
|
|
}
|
|
|
|
+ private uint nr_nonempty_children () {
|
|
+ uint result = 0;
|
|
+ for (uint i = 0; i < this.elements.length; i++) {
|
|
+ if (!this.elements[i].is_empty)
|
|
+ result++;
|
|
+ }
|
|
+ return result;
|
|
+ }
|
|
+
|
|
public override Value? to_value () {
|
|
var afds = new Gee.HashSet<AbstractFieldDetails> ();
|
|
for (uint i = 0; i < this.elements.length; i++) {
|
|
@@ -117,6 +153,21 @@ public abstract class Contacts.BinChunk : Chunk, GLib.ListModel {
|
|
return afds;
|
|
}
|
|
|
|
+ /**
|
|
+ * A helper finish the initialization of a BinChunk. It makes sure to set the
|
|
+ * "original_elements" field (which is used to calculate the "dirty"
|
|
+ * property) as well as doing an initial emptiness check
|
|
+ */
|
|
+ protected void finish_initialization () {
|
|
+ // Make a deep copy to ensure changes don't propagate to original_elements
|
|
+ this.original_elements = this.elements.copy ((child) => {
|
|
+ return child.copy ();
|
|
+ }).steal ();
|
|
+ this.original_elements_set = true;
|
|
+
|
|
+ emptiness_check ();
|
|
+ }
|
|
+
|
|
// ListModel implementation
|
|
|
|
public uint n_items { get { return this.elements.length; } }
|
|
@@ -163,6 +214,19 @@ public abstract class Contacts.BinChunkChild : GLib.Object {
|
|
*/
|
|
public abstract AbstractFieldDetails? create_afd ();
|
|
|
|
+ /**
|
|
+ * Creates a deep copy of this child
|
|
+ */
|
|
+ public abstract BinChunkChild copy ();
|
|
+
|
|
+ // Helper to copy this object's parameters field into that of @copy
|
|
+ protected void copy_parameters (BinChunkChild copy) {
|
|
+ copy.parameters.clear ();
|
|
+ var iter = this.parameters.map_iterator ();
|
|
+ while (iter.next ())
|
|
+ copy.parameters[iter.get_key ()] = iter.get_value ();
|
|
+ }
|
|
+
|
|
// A helper to change a string field with the proper propery notifies
|
|
protected void change_string_prop (string prop_name,
|
|
ref string old_value,
|
|
@@ -180,8 +244,8 @@ public abstract class Contacts.BinChunkChild : GLib.Object {
|
|
}
|
|
|
|
/**
|
|
- * Compares 2 children in an intuitive manner, so that preferred children go
|
|
- * first and empty children are last
|
|
+ * Compares 2 children in such a way that unequal children are sorted in an
|
|
+ * intuitive manner
|
|
*/
|
|
public int compare (BinChunkChild other) {
|
|
// Fields with a PREF hint always go first (see vCard PREF attribute)
|
|
@@ -195,7 +259,7 @@ public abstract class Contacts.BinChunkChild : GLib.Object {
|
|
return empty? 1 : -1;
|
|
|
|
// FIXME: maybe also compare the types? (e.g. put HOME before WORK)
|
|
- return 0;
|
|
+ return compare_internal (other);
|
|
}
|
|
|
|
/**
|
|
@@ -213,4 +277,44 @@ public abstract class Contacts.BinChunkChild : GLib.Object {
|
|
}
|
|
return false;
|
|
}
|
|
+
|
|
+ /**
|
|
+ * Should be implemented by subclasses to compare with logic specific to that
|
|
+ * property. Note that we ideally try to go for a stable sort
|
|
+ */
|
|
+ protected abstract int compare_internal (BinChunkChild other);
|
|
+
|
|
+ // Helper to do a very dumb ordering with this function
|
|
+ protected int dummy_compare_parameters (BinChunkChild other) {
|
|
+ // TYPE is a special vcard param, so use that
|
|
+ var this_types = this.parameters["type"].to_array ();
|
|
+ var other_types = other.parameters["type"].to_array ();
|
|
+
|
|
+ // If one type is more specific than the other, use that
|
|
+ if (this_types.length != other_types.length)
|
|
+ return other_types.length - this_types.length;
|
|
+
|
|
+ for (uint i = 0; i < this_types.length; i++) {
|
|
+ var type_cmp = strcmp (this_types[i], other_types[i]);
|
|
+ if (type_cmp != 0)
|
|
+ return type_cmp;
|
|
+ }
|
|
+
|
|
+ // If the number of parameters is larger, assume it's more specific
|
|
+ // so put it up front
|
|
+ if (this.parameters.size != other.parameters.size)
|
|
+ return other.parameters.size - this.parameters.size;
|
|
+
|
|
+ // Go over all parameters and check for any difference in size
|
|
+ var keys = this.parameters.get_keys ();
|
|
+ foreach (string key in keys) {
|
|
+ var this_params = this.parameters[key];
|
|
+ var other_params = other.parameters[key];
|
|
+
|
|
+ if (this_params.size != other_params.size)
|
|
+ return other_params.size - this_params.size;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+ }
|
|
}
|
|
diff --git a/src/core/contacts-birthday-chunk.vala b/src/core/contacts-birthday-chunk.vala
|
|
index 087da6a6..d929dc5f 100644
|
|
--- a/src/core/contacts-birthday-chunk.vala
|
|
+++ b/src/core/contacts-birthday-chunk.vala
|
|
@@ -23,19 +23,25 @@ using Folks;
|
|
*/
|
|
public class Contacts.BirthdayChunk : Chunk {
|
|
|
|
+ private DateTime? original_birthday = null;
|
|
+
|
|
public DateTime? birthday {
|
|
get { return this._birthday; }
|
|
set {
|
|
- if (this._birthday == null && value == null)
|
|
+ if (this.birthday == null && value == null)
|
|
return;
|
|
|
|
- if (this._birthday != null && value != null
|
|
- && this._birthday.equal (value.to_utc ()))
|
|
+ if (this.birthday != null && value != null && this.birthday.equal (value))
|
|
return;
|
|
|
|
+ bool was_empty = this.is_empty;
|
|
+ bool was_dirty = this.dirty;
|
|
this._birthday = (value != null)? value.to_utc () : null;
|
|
notify_property ("birthday");
|
|
- notify_property ("is-empty");
|
|
+ if (was_empty != this.is_empty)
|
|
+ notify_property ("is-empty");
|
|
+ if (was_dirty != this.dirty)
|
|
+ notify_property ("dirty");
|
|
}
|
|
}
|
|
private DateTime? _birthday = null;
|
|
@@ -44,11 +50,21 @@ public class Contacts.BirthdayChunk : Chunk {
|
|
|
|
public override bool is_empty { get { return this.birthday == null; } }
|
|
|
|
+ public override bool dirty {
|
|
+ get {
|
|
+ if (this.birthday != null && this.original_birthday != null)
|
|
+ return !this.birthday.equal (this.original_birthday);
|
|
+ return this.birthday != this.original_birthday;
|
|
+ }
|
|
+ }
|
|
+
|
|
construct {
|
|
if (persona != null) {
|
|
return_if_fail (persona is BirthdayDetails);
|
|
- persona.bind_property ("birthday", this, "birthday", BindingFlags.SYNC_CREATE);
|
|
+ persona.bind_property ("birthday", this, "birthday");
|
|
+ this._birthday = ((BirthdayDetails) persona).birthday;
|
|
}
|
|
+ this.original_birthday = this.birthday;
|
|
}
|
|
|
|
public override Value? to_value () {
|
|
diff --git a/src/core/contacts-chunk.vala b/src/core/contacts-chunk.vala
|
|
index 998ad690..328a0424 100644
|
|
--- a/src/core/contacts-chunk.vala
|
|
+++ b/src/core/contacts-chunk.vala
|
|
@@ -42,10 +42,11 @@ public abstract class Contacts.Chunk : GLib.Object {
|
|
public abstract bool is_empty { get; }
|
|
|
|
/**
|
|
- * A separate field to keep track of whether something has changed.
|
|
- * If it did, we know we'll have to (possibly) save the changes.
|
|
+ * A separate field to keep track of whether this has changed from its
|
|
+ * original value. If it did, we know we'll have to (possibly) save the
|
|
+ * changes.
|
|
*/
|
|
- public bool changed { get; protected set; default = false; }
|
|
+ public abstract bool dirty { get; }
|
|
|
|
/**
|
|
* Converts this chunk into a GLib.Value, as expected by API like
|
|
diff --git a/src/core/contacts-contact.vala b/src/core/contacts-contact.vala
|
|
index 5fcc7425..761f447b 100644
|
|
--- a/src/core/contacts-contact.vala
|
|
+++ b/src/core/contacts-contact.vala
|
|
@@ -273,6 +273,12 @@ public class Contacts.Contact : GLib.Object, GLib.ListModel {
|
|
if (individual == null)
|
|
individual = chunk.persona.individual;
|
|
|
|
+ if (!chunk.dirty) {
|
|
+ debug ("Not saving unchanged property '%s' to persona %s",
|
|
+ chunk.property_name, chunk.persona.uid);
|
|
+ continue;
|
|
+ }
|
|
+
|
|
if (!(chunk.property_name in chunk.persona.writeable_properties)) {
|
|
warning ("Can't save to unwriteable property '%s' to persona %s",
|
|
chunk.property_name, chunk.persona.uid);
|
|
diff --git a/src/core/contacts-email-addresses-chunk.vala b/src/core/contacts-email-addresses-chunk.vala
|
|
index 1119a2cb..36f57156 100644
|
|
--- a/src/core/contacts-email-addresses-chunk.vala
|
|
+++ b/src/core/contacts-email-addresses-chunk.vala
|
|
@@ -32,7 +32,7 @@ public class Contacts.EmailAddressesChunk : BinChunk {
|
|
}
|
|
}
|
|
|
|
- emptiness_check ();
|
|
+ finish_initialization ();
|
|
}
|
|
|
|
protected override BinChunkChild create_empty_child () {
|
|
@@ -72,6 +72,15 @@ public class Contacts.EmailAddress : BinChunkChild {
|
|
this.parameters = email_field.parameters;
|
|
}
|
|
|
|
+ protected override int compare_internal (BinChunkChild other)
|
|
+ requires (other is EmailAddress) {
|
|
+ unowned var other_email_addr = (EmailAddress) other;
|
|
+ var addr_cmp = strcmp (this.raw_address, other_email_addr.raw_address);
|
|
+ if (addr_cmp != 0)
|
|
+ return addr_cmp;
|
|
+ return dummy_compare_parameters (other);
|
|
+ }
|
|
+
|
|
/**
|
|
* Returns the TypeDescriptor that describes the type of the email address
|
|
* (for example personal, work, ...)
|
|
@@ -86,6 +95,12 @@ public class Contacts.EmailAddress : BinChunkChild {
|
|
|
|
return new EmailFieldDetails (this.raw_address, this.parameters);
|
|
}
|
|
+ public override BinChunkChild copy () {
|
|
+ var email_address = new EmailAddress ();
|
|
+ email_address.raw_address = this.raw_address;
|
|
+ copy_parameters (email_address);
|
|
+ return email_address;
|
|
+ }
|
|
|
|
public string get_mailto_uri () {
|
|
return "mailto:" + Uri.escape_string (this.raw_address, "@" , false);
|
|
diff --git a/src/core/contacts-full-name-chunk.vala b/src/core/contacts-full-name-chunk.vala
|
|
index 647f5561..e59fb382 100644
|
|
--- a/src/core/contacts-full-name-chunk.vala
|
|
+++ b/src/core/contacts-full-name-chunk.vala
|
|
@@ -24,6 +24,8 @@ using Folks;
|
|
*/
|
|
public class Contacts.FullNameChunk : Chunk {
|
|
|
|
+ private string original_full_name = "";
|
|
+
|
|
public string full_name {
|
|
get { return this._full_name; }
|
|
set {
|
|
@@ -31,10 +33,13 @@ public class Contacts.FullNameChunk : Chunk {
|
|
return;
|
|
|
|
bool was_empty = this.is_empty;
|
|
+ bool was_dirty = this.dirty;
|
|
this._full_name = value;
|
|
notify_property ("full-name");
|
|
if (this.is_empty != was_empty)
|
|
notify_property ("is-empty");
|
|
+ if (was_dirty != this.dirty)
|
|
+ notify_property ("dirty");
|
|
}
|
|
}
|
|
private string _full_name = "";
|
|
@@ -43,11 +48,17 @@ public class Contacts.FullNameChunk : Chunk {
|
|
|
|
public override bool is_empty { get { return this._full_name.strip () == ""; } }
|
|
|
|
+ public override bool dirty {
|
|
+ get { return this.full_name.strip () != this.original_full_name.strip (); }
|
|
+ }
|
|
+
|
|
construct {
|
|
if (persona != null) {
|
|
return_if_fail (persona is NameDetails);
|
|
- persona.bind_property ("full-name", this, "full-name", BindingFlags.SYNC_CREATE);
|
|
+ persona.bind_property ("full-name", this, "full-name");
|
|
+ this._full_name = ((NameDetails) persona).full_name;
|
|
}
|
|
+ this.original_full_name = this.full_name;
|
|
}
|
|
|
|
public override Value? to_value () {
|
|
diff --git a/src/core/contacts-im-addresses-chunk.vala b/src/core/contacts-im-addresses-chunk.vala
|
|
index 031f8045..95cdd3ad 100644
|
|
--- a/src/core/contacts-im-addresses-chunk.vala
|
|
+++ b/src/core/contacts-im-addresses-chunk.vala
|
|
@@ -39,7 +39,7 @@ public class Contacts.ImAddressesChunk : BinChunk {
|
|
}
|
|
}
|
|
|
|
- emptiness_check ();
|
|
+ finish_initialization ();
|
|
}
|
|
|
|
protected override BinChunkChild create_empty_child () {
|
|
@@ -90,10 +90,33 @@ public class Contacts.ImAddress : BinChunkChild {
|
|
this.parameters = im_field.parameters;
|
|
}
|
|
|
|
+ protected override int compare_internal (BinChunkChild other)
|
|
+ requires (other is ImAddress) {
|
|
+ unowned var other_im_addr = (ImAddress) other;
|
|
+
|
|
+ var protocol_cmp = strcmp (this.protocol, other_im_addr.protocol);
|
|
+ if (protocol_cmp != 0)
|
|
+ return protocol_cmp;
|
|
+
|
|
+ var addr_cmp = strcmp (this.address, other_im_addr.address);
|
|
+ if (addr_cmp != 0)
|
|
+ return addr_cmp;
|
|
+
|
|
+ return dummy_compare_parameters (other);
|
|
+ }
|
|
+
|
|
public override AbstractFieldDetails? create_afd () {
|
|
if (this.is_empty)
|
|
return null;
|
|
|
|
return new ImFieldDetails (this.address, this.parameters);
|
|
}
|
|
+
|
|
+ public override BinChunkChild copy () {
|
|
+ var ima = new ImAddress ();
|
|
+ ima.protocol = this.protocol;
|
|
+ ima.address = this.address;
|
|
+ copy_parameters (ima);
|
|
+ return ima;
|
|
+ }
|
|
}
|
|
diff --git a/src/core/contacts-nickname-chunk.vala b/src/core/contacts-nickname-chunk.vala
|
|
index ba505f08..81cf1d98 100644
|
|
--- a/src/core/contacts-nickname-chunk.vala
|
|
+++ b/src/core/contacts-nickname-chunk.vala
|
|
@@ -22,6 +22,8 @@ using Folks;
|
|
*/
|
|
public class Contacts.NicknameChunk : Chunk {
|
|
|
|
+ private string original_nickname = "";
|
|
+
|
|
public string nickname {
|
|
get { return this._nickname; }
|
|
set {
|
|
@@ -29,10 +31,13 @@ public class Contacts.NicknameChunk : Chunk {
|
|
return;
|
|
|
|
bool was_empty = this.is_empty;
|
|
+ bool was_dirty = this.dirty;
|
|
this._nickname = value;
|
|
notify_property ("nickname");
|
|
if (this.is_empty != was_empty)
|
|
notify_property ("is-empty");
|
|
+ if (was_dirty != this.dirty)
|
|
+ notify_property ("dirty");
|
|
}
|
|
}
|
|
private string _nickname = "";
|
|
@@ -41,11 +46,17 @@ public class Contacts.NicknameChunk : Chunk {
|
|
|
|
public override bool is_empty { get { return this._nickname.strip () == ""; } }
|
|
|
|
+ public override bool dirty {
|
|
+ get { return this.nickname.strip () != this.original_nickname.strip (); }
|
|
+ }
|
|
+
|
|
construct {
|
|
if (persona != null) {
|
|
return_if_fail (persona is NameDetails);
|
|
- persona.bind_property ("nickname", this, "nickname", BindingFlags.SYNC_CREATE);
|
|
+ persona.bind_property ("nickname", this, "nickname");
|
|
+ this._nickname = ((NameDetails) persona).nickname;
|
|
}
|
|
+ this.original_nickname = this.nickname;
|
|
}
|
|
|
|
public override Value? to_value () {
|
|
diff --git a/src/core/contacts-notes-chunk.vala b/src/core/contacts-notes-chunk.vala
|
|
index 45b5c43b..2f1ee3ae 100644
|
|
--- a/src/core/contacts-notes-chunk.vala
|
|
+++ b/src/core/contacts-notes-chunk.vala
|
|
@@ -36,7 +36,7 @@ public class Contacts.NotesChunk : BinChunk {
|
|
}
|
|
}
|
|
|
|
- emptiness_check ();
|
|
+ finish_initialization ();
|
|
}
|
|
|
|
protected override BinChunkChild create_empty_child () {
|
|
@@ -76,10 +76,22 @@ public class Contacts.Note : BinChunkChild {
|
|
this.parameters = note_field.parameters;
|
|
}
|
|
|
|
+ protected override int compare_internal (BinChunkChild other)
|
|
+ requires (other is Note) {
|
|
+ return strcmp (this.text, ((Note) other).text);
|
|
+ }
|
|
+
|
|
public override AbstractFieldDetails? create_afd () {
|
|
if (this.is_empty)
|
|
return null;
|
|
|
|
return new NoteFieldDetails (this.text, this.parameters);
|
|
}
|
|
+
|
|
+ public override BinChunkChild copy () {
|
|
+ var note = new Note ();
|
|
+ note.text = this.text;
|
|
+ copy_parameters (note);
|
|
+ return note;
|
|
+ }
|
|
}
|
|
diff --git a/src/core/contacts-phones-chunk.vala b/src/core/contacts-phones-chunk.vala
|
|
index 8135d98a..c8e0ce3a 100644
|
|
--- a/src/core/contacts-phones-chunk.vala
|
|
+++ b/src/core/contacts-phones-chunk.vala
|
|
@@ -36,7 +36,7 @@ public class Contacts.PhonesChunk : BinChunk {
|
|
}
|
|
}
|
|
|
|
- emptiness_check ();
|
|
+ finish_initialization ();
|
|
}
|
|
|
|
protected override BinChunkChild create_empty_child () {
|
|
@@ -80,6 +80,15 @@ public class Contacts.Phone : BinChunkChild {
|
|
this.parameters = phone_field.parameters;
|
|
}
|
|
|
|
+ protected override int compare_internal (BinChunkChild other)
|
|
+ requires (other is Phone) {
|
|
+ unowned var other_phone = (Phone) other;
|
|
+ var nr_cmp = strcmp (this.raw_number, other_phone.raw_number);
|
|
+ if (nr_cmp != 0)
|
|
+ return nr_cmp;
|
|
+ return dummy_compare_parameters (other);
|
|
+ }
|
|
+
|
|
/**
|
|
* Returns the TypeDescriptor that describes the type of phone number
|
|
* (for example mobile, work, fax, ...)
|
|
@@ -94,4 +103,11 @@ public class Contacts.Phone : BinChunkChild {
|
|
|
|
return new PhoneFieldDetails (this.raw_number, this.parameters);
|
|
}
|
|
+
|
|
+ public override BinChunkChild copy () {
|
|
+ var phone = new Phone ();
|
|
+ phone.raw_number = this.raw_number;
|
|
+ copy_parameters (phone);
|
|
+ return phone;
|
|
+ }
|
|
}
|
|
diff --git a/src/core/contacts-roles-chunk.vala b/src/core/contacts-roles-chunk.vala
|
|
index bec585b2..948c42b9 100644
|
|
--- a/src/core/contacts-roles-chunk.vala
|
|
+++ b/src/core/contacts-roles-chunk.vala
|
|
@@ -37,7 +37,7 @@ public class Contacts.RolesChunk : BinChunk {
|
|
}
|
|
}
|
|
|
|
- emptiness_check ();
|
|
+ finish_initialization ();
|
|
}
|
|
|
|
protected override BinChunkChild create_empty_child () {
|
|
@@ -72,6 +72,16 @@ public class Contacts.OrgRole : BinChunkChild {
|
|
this.parameters = role_field.parameters;
|
|
}
|
|
|
|
+ protected override int compare_internal (BinChunkChild other)
|
|
+ requires (other is OrgRole) {
|
|
+ unowned var other_orgrole = (OrgRole) other;
|
|
+ var orgs_cmp = strcmp (this.role.organisation_name,
|
|
+ other_orgrole.role.organisation_name);
|
|
+ if (orgs_cmp != 0)
|
|
+ return orgs_cmp;
|
|
+ return strcmp (this.role.title, other_orgrole.role.title);
|
|
+ }
|
|
+
|
|
public override AbstractFieldDetails? create_afd () {
|
|
if (this.is_empty)
|
|
return null;
|
|
@@ -79,6 +89,15 @@ public class Contacts.OrgRole : BinChunkChild {
|
|
return new RoleFieldDetails (this.role, this.parameters);
|
|
}
|
|
|
|
+ public override BinChunkChild copy () {
|
|
+ var org_role = new OrgRole ();
|
|
+ org_role.role.organisation_name = this.role.organisation_name;
|
|
+ org_role.role.role = this.role.role;
|
|
+ org_role.role.title = this.role.title;
|
|
+ copy_parameters (org_role);
|
|
+ return org_role;
|
|
+ }
|
|
+
|
|
public string to_string () {
|
|
if (this.role.title != "") {
|
|
if (this.role.organisation_name != "") {
|
|
diff --git a/src/core/contacts-structured-name-chunk.vala b/src/core/contacts-structured-name-chunk.vala
|
|
index 07cbc8f9..388aa9f1 100644
|
|
--- a/src/core/contacts-structured-name-chunk.vala
|
|
+++ b/src/core/contacts-structured-name-chunk.vala
|
|
@@ -25,6 +25,8 @@ using Folks;
|
|
*/
|
|
public class Contacts.StructuredNameChunk : Chunk {
|
|
|
|
+ private StructuredName original_structured_name;
|
|
+
|
|
public StructuredName structured_name {
|
|
get { return this._structured_name; }
|
|
set {
|
|
@@ -51,11 +53,17 @@ public class Contacts.StructuredNameChunk : Chunk {
|
|
}
|
|
}
|
|
|
|
+ public override bool dirty {
|
|
+ get { return !this.original_structured_name.equal (this._structured_name); }
|
|
+ }
|
|
+
|
|
construct {
|
|
if (persona != null) {
|
|
return_if_fail (persona is NameDetails);
|
|
- persona.bind_property ("structured-name", this, "structured-name", BindingFlags.SYNC_CREATE);
|
|
+ persona.bind_property ("structured-name", this, "structured-name");
|
|
+ this._structured_name = ((NameDetails) persona).structured_name;
|
|
}
|
|
+ this.original_structured_name = this.structured_name;
|
|
}
|
|
|
|
public override Value? to_value () {
|
|
diff --git a/src/core/contacts-urls-chunk.vala b/src/core/contacts-urls-chunk.vala
|
|
index 671fc4dd..62b02c0e 100644
|
|
--- a/src/core/contacts-urls-chunk.vala
|
|
+++ b/src/core/contacts-urls-chunk.vala
|
|
@@ -36,7 +36,7 @@ public class Contacts.UrlsChunk : BinChunk {
|
|
}
|
|
}
|
|
|
|
- emptiness_check ();
|
|
+ finish_initialization ();
|
|
}
|
|
|
|
protected override BinChunkChild create_empty_child () {
|
|
@@ -76,6 +76,11 @@ public class Contacts.Url : BinChunkChild {
|
|
this.parameters = url_field.parameters;
|
|
}
|
|
|
|
+ protected override int compare_internal (BinChunkChild other)
|
|
+ requires (other is Url) {
|
|
+ return strcmp (this.raw_url, ((Url) other).raw_url);
|
|
+ }
|
|
+
|
|
/**
|
|
* Tries to return an absolute URL (with a scheme).
|
|
* Since we know contact URL values are for web addresses, we try to fall
|
|
@@ -92,4 +97,11 @@ public class Contacts.Url : BinChunkChild {
|
|
|
|
return new UrlFieldDetails (this.raw_url, this.parameters);
|
|
}
|
|
+
|
|
+ public override BinChunkChild copy () {
|
|
+ var url = new Url ();
|
|
+ url.raw_url = this.raw_url;
|
|
+ copy_parameters (url);
|
|
+ return url;
|
|
+ }
|
|
}
|
|
--
|
|
GitLab
|
|
|
|
|
|
From a84eae026f1869c2de083db7f04472a84e017fa9 Mon Sep 17 00:00:00 2001
|
|
From: Niels De Graef <nielsdegraef@gmail.com>
|
|
Date: Mon, 10 Oct 2022 20:16:45 +0200
|
|
Subject: [PATCH 2/2] contact: Copy the chunks before applying changes
|
|
|
|
When applying the changes of certain fields, we've seen that this leads
|
|
to a `individuals_changed_detailed()` being called with the same
|
|
individual in the `removed` and `added` set. The signal callback
|
|
propagates to several layers, until it lands in the
|
|
`Contact:on_individual_personas_changed()` function. There, all chunks
|
|
related to the persona are removed, even when we still might be applying
|
|
changes of some of the other chunks.
|
|
|
|
The `apply_changes()` method in other words should keep its own copy to
|
|
prevent that.
|
|
|
|
Fixes: https://gitlab.gnome.org/GNOME/gnome-contacts/-/issues/271
|
|
---
|
|
src/core/contacts-contact.vala | 11 +++++++----
|
|
1 file changed, 7 insertions(+), 4 deletions(-)
|
|
|
|
diff --git a/src/core/contacts-contact.vala b/src/core/contacts-contact.vala
|
|
index 761f447b..742bab71 100644
|
|
--- a/src/core/contacts-contact.vala
|
|
+++ b/src/core/contacts-contact.vala
|
|
@@ -264,9 +264,12 @@ public class Contacts.Contact : GLib.Object, GLib.ListModel {
|
|
public async unowned Individual? apply_changes (PersonaStore store) throws GLib.Error {
|
|
unowned Individual? individual = null;
|
|
|
|
+ // Create a (shallow) copy of the chunks
|
|
+ var chunks = this.chunks.copy ((chunk) => { return chunk; });
|
|
+
|
|
// For those that were a persona: save the properties using the API
|
|
- for (uint i = 0; i < this.chunks.length; i++) {
|
|
- unowned var chunk = this.chunks[i];
|
|
+ for (uint i = 0; i < chunks.length; i++) {
|
|
+ unowned var chunk = chunks[i];
|
|
if (chunk.persona == null)
|
|
continue;
|
|
|
|
@@ -297,8 +300,8 @@ public class Contacts.Contact : GLib.Object, GLib.ListModel {
|
|
|
|
// Find those without a persona, and save them into the primary store
|
|
var new_details = new HashTable<string, Value?> (str_hash, str_equal);
|
|
- for (uint i = 0; i < this.chunks.length; i++) {
|
|
- unowned var chunk = this.chunks[i];
|
|
+ for (uint i = 0; i < chunks.length; i++) {
|
|
+ unowned var chunk = chunks[i];
|
|
if (chunk.persona != null)
|
|
continue;
|
|
|
|
--
|
|
GitLab
|
|
|