diff --git a/213.patch b/213.patch new file mode 100644 index 0000000..16e9647 --- /dev/null +++ b/213.patch @@ -0,0 +1,84 @@ +From 0e4907177d80097b29d74b1a8af3aeed36074196 Mon Sep 17 00:00:00 2001 +From: Niels De Graef +Date: Thu, 29 Sep 2022 18:41:21 +0200 +Subject: [PATCH] pane: Select the new individual after saving changes + +Fixes: https://gitlab.gnome.org/GNOME/gnome-contacts/-/issues/271 +--- + src/contacts-contact-pane.vala | 16 ++++++++++++++-- + src/core/contacts-contact.vala | 14 +++++++++++++- + 2 files changed, 27 insertions(+), 3 deletions(-) + +diff --git a/src/contacts-contact-pane.vala b/src/contacts-contact-pane.vala +index ffdcbf33..ee9ee6c3 100644 +--- a/src/contacts-contact-pane.vala ++++ b/src/contacts-contact-pane.vala +@@ -176,12 +176,24 @@ public class Contacts.ContactPane : Adw.Bin { + } + + try { +- yield contact.apply_changes (this.store.aggregator.primary_store); ++ // The new individual. Even when editing an exisiting contact, it might ++ // be a different Individual than before, so make sure to adjust our ++ // selected contact afterwards ++ unowned var individual = ++ yield contact.apply_changes (this.store.aggregator.primary_store); ++ debug ("Applied changes resulted in individual (%s)", ++ (individual != null)? individual.id : "null"); ++ ++ if (individual != null) { ++ var pos = yield this.store.find_individual_for_id (individual.id); ++ if (pos != Gtk.INVALID_LIST_POSITION) ++ this.store.selection.selected = pos; ++ } + } catch (Error err) { + warning ("Couldn't save changes: %s", err.message); ++ show_contact (null); + // XXX do something better here + } +- show_contact_sheet (contact); + } + + public void edit_contact () { +diff --git a/src/core/contacts-contact.vala b/src/core/contacts-contact.vala +index 866ec187..5fcc7425 100644 +--- a/src/core/contacts-contact.vala ++++ b/src/core/contacts-contact.vala +@@ -258,14 +258,21 @@ public class Contacts.Contact : GLib.Object, GLib.ListModel { + * Applies any pending changes to all chunks. This can mean either a new + * persona is made, or it is saved in the chunk's referenced persona. + * When a new persona is made, it will be added to @store. ++ * ++ * Returns the Individual that was created from applying the changes + */ +- public async void apply_changes (PersonaStore store) throws GLib.Error { ++ public async unowned Individual? apply_changes (PersonaStore store) throws GLib.Error { ++ unowned Individual? individual = null; ++ + // 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]; + if (chunk.persona == null) + continue; + ++ if (individual == null) ++ individual = chunk.persona.individual; ++ + 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); +@@ -303,6 +310,11 @@ public class Contacts.Contact : GLib.Object, GLib.ListModel { + var persona = yield store.add_persona_from_details (new_details); + debug ("Successfully created new persona %p", persona); + // FIXME: should we set the persona for these chunks? ++ ++ if (individual == null && persona != null) ++ individual = persona.individual; + } ++ ++ return individual; + } + } +-- +GitLab + diff --git a/214.patch b/214.patch new file mode 100644 index 0000000..887dbd7 --- /dev/null +++ b/214.patch @@ -0,0 +1,883 @@ +From 380136c4e39f1f6d5ce5894b2b9f3506c3a9d1a6 Mon Sep 17 00:00:00 2001 +From: Niels De Graef +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 elements = new GenericArray (); + + 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 (); + 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 +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 (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 + diff --git a/216.patch b/216.patch new file mode 100644 index 0000000..2dc0650 --- /dev/null +++ b/216.patch @@ -0,0 +1,29 @@ +From 5565cd0961aaa204599e0af57fd8806a65758c10 Mon Sep 17 00:00:00 2001 +From: Niels De Graef +Date: Fri, 14 Oct 2022 08:56:30 +0200 +Subject: [PATCH] main-window: Hide the link button + +There's definitely something going wrong with linking contacts, as it +forgets any link between app restarts. Since we really don't want to +expose something that's completely broken to our users, hide the button +that allows the linking. It's a shitty workaround for now. +--- + data/ui/contacts-main-window.ui | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/data/ui/contacts-main-window.ui b/data/ui/contacts-main-window.ui +index 3d01bdc7..fd8f0a17 100644 +--- a/data/ui/contacts-main-window.ui ++++ b/data/ui/contacts-main-window.ui +@@ -167,7 +167,7 @@ + + + +- False ++ False + Link + Link Selected Contacts Together + win.link-marked-contacts +-- +GitLab + diff --git a/gnome-contacts-42.0.tar.xz b/gnome-contacts-42.0.tar.xz deleted file mode 100644 index e87afb8..0000000 Binary files a/gnome-contacts-42.0.tar.xz and /dev/null differ diff --git a/gnome-contacts-43.0.tar.xz b/gnome-contacts-43.0.tar.xz new file mode 100644 index 0000000..339ecd7 Binary files /dev/null and b/gnome-contacts-43.0.tar.xz differ diff --git a/gnome-contacts.spec b/gnome-contacts.spec index f31437c..3c2f2ae 100644 --- a/gnome-contacts.spec +++ b/gnome-contacts.spec @@ -1,31 +1,47 @@ %global gtk4_version 4.6 Name: gnome-contacts -Version: 42.0 +Version: 43.0 Release: 1 -Summary: Integrated address book for GNOME +Summary: Contacts manager for GNOME License: GPLv2+ URL: https://wiki.gnome.org/Apps/Contacts -Source0: https://download.gnome.org/sources/%{name}/42/%{name}-%{version}.tar.xz +Source0: https://download.gnome.org/sources/%{name}/43/%{name}-%{version}.tar.xz -BuildRequires: desktop-file-utils docbook-dtds docbook-style-xsl gettext meson vala libappstream-glib -BuildRequires: libxslt pkgconfig(folks) >= 0.11.4 pkgconfig(folks-eds) -BuildRequires: pkgconfig(folks-telepathy) pkgconfig(gee-0.8) pkgconfig(goa-1.0) pkgconfig(libadwaita-1) -BuildRequires: pkgconfig(gobject-introspection-1.0) pkgconfig(libportal) +Patch0: 213.patch +Patch1: 214.patch +Patch2: 216.patch + +BuildRequires: desktop-file-utils +BuildRequires: docbook-dtds +BuildRequires: docbook-style-xsl +BuildRequires: gettext +BuildRequires: meson +BuildRequires: vala +BuildRequires: libappstream-glib +BuildRequires: libxslt +BuildRequires: pkgconfig(folks) +BuildRequires: pkgconfig(folks-eds) +BuildRequires: pkgconfig(gee-0.8) +BuildRequires: pkgconfig(goa-1.0) +BuildRequires: pkgconfig(gobject-introspection-1.0) BuildRequires: pkgconfig(gtk4) >= %{gtk4_version} +BuildRequires: pkgconfig(libadwaita-1) +BuildRequires: pkgconfig(libportal-gtk4) -Requires: folks >= 1:0.11.4 gtk4%{?_isa} >= %{gtk4_version} hicolor-icon-theme +Requires: gtk4%{?_isa} >= %{gtk4_version} +Requires: hicolor-icon-theme %description -Contacts is GNOME's integrated address book. It is written in Vala and uses libfolks -(also written in Vala) and Evolution Data Server. +%{name} is a standalone contacts manager for GNOME desktop. %package_help %prep -%autosetup -n %{name}-%{version} +%autosetup -p1 -n %{name}-%{version} %build +export VALAFLAGS="-g" %meson %meson_build @@ -40,11 +56,14 @@ desktop-file-validate %{buildroot}/%{_datadir}/applications/org.gnome.Contacts.d %files -f %{name}.lang %license COPYING %{_bindir}/gnome-contacts +%{_libexecdir}/gnome-contacts/ %{_libexecdir}/gnome-contacts-search-provider %{_datadir}/applications/org.gnome.Contacts.desktop %{_datadir}/dbus-1/services/org.gnome.Contacts.service %{_datadir}/dbus-1/services/org.gnome.Contacts.SearchProvider.service %{_datadir}/glib-2.0/schemas/org.gnome.Contacts.gschema.xml +%dir %{_datadir}/gnome-shell +%dir %{_datadir}/gnome-shell/search-providers %{_datadir}/gnome-shell/search-providers/org.gnome.Contacts.search-provider.ini %{_datadir}/icons/hicolor/*/apps/org.gnome.Contacts*.svg %{_datadir}/metainfo/org.gnome.Contacts.appdata.xml @@ -54,6 +73,9 @@ desktop-file-validate %{buildroot}/%{_datadir}/applications/org.gnome.Contacts.d %{_mandir}/man1/gnome-contacts.1* %changelog +* Mon Jan 02 2023 lin zhang - 43.0-1 +- Update to 43.0 + * Mon Mar 28 2022 lin zhang - 42.0-1 - Update to 42.0